diff --git a/.changeset/prisma-next-drop-encrypted-prefix.md b/.changeset/prisma-next-drop-encrypted-prefix.md new file mode 100644 index 00000000..dfda43e8 --- /dev/null +++ b/.changeset/prisma-next-drop-encrypted-prefix.md @@ -0,0 +1,26 @@ +--- +'@cipherstash/prisma-next': minor +'stash': patch +--- + +**Breaking (v3 authoring surface):** the EQL v3 PSL column constructors drop +the `Encrypted` prefix to line up with the stack / Drizzle `types.*` catalog — +the `cipherstash.` namespace already disambiguates. So +`cipherstash.EncryptedTextSearch()` → `cipherstash.TextSearch()`, +`cipherstash.EncryptedDoubleOrd()` → `cipherstash.DoubleOrd()`, +`cipherstash.EncryptedBoolean()` → `cipherstash.Boolean()`, etc. + +The v3 one-call setup function is renamed `cipherstashFromStackV3` → +`cipherstashFromStack` (v3 is the default), and the existing v2 setup function +becomes `cipherstashFromStackV2`. + +The camelCase TS-authoring factory exports move in lockstep: +`encryptedTextSearch` → `textSearch`, `encryptedDoubleOrd` → `doubleOrd`, etc. +(a property test enforces the PSL and TS names agree modulo first-letter case). + +Unchanged: the runtime value envelopes (`EncryptedString`, `EncryptedNumber`, +`EncryptedBoolean`, …), the `cipherstash.*V2` legacy column constructors, the +generated `contract.json` / codec ids, and the `eql*` query operators. + +The `stash-prisma-next` skill is updated to the new names (skills ship in the +`stash` tarball). diff --git a/examples/prisma/README.md b/examples/prisma/README.md index 723e6455..bc6445c4 100644 --- a/examples/prisma/README.md +++ b/examples/prisma/README.md @@ -6,12 +6,12 @@ A single `User` model with one column per plaintext family, exercised end-to-end | Column | Constructor | Domain | Query surface | | --------------- | --------------------------------- | --------------------------- | -------------------------------------- | -| `email` | `cipherstash.EncryptedTextSearch()` | `public.eql_v3_text_search` | equality + order/range + free-text (`eqlMatch`) | -| `salary` | `cipherstash.EncryptedDoubleOrd()` | `public.eql_v3_double_ord` | equality + order/range | -| `accountId` | `cipherstash.EncryptedBigIntOrd()` | `public.eql_v3_bigint_ord` | equality + order/range (true `bigint`) | -| `birthday` | `cipherstash.EncryptedDateOrd()` | `public.eql_v3_date_ord` | equality + order/range | -| `emailVerified` | `cipherstash.EncryptedBoolean()` | `public.eql_v3_boolean` | storage-only (no operators) | -| `preferences` | `cipherstash.EncryptedJson()` | `public.eql_v3_json` | `eqlJsonContains` (`@>`) | +| `email` | `cipherstash.TextSearch()` | `public.eql_v3_text_search` | equality + order/range + free-text (`eqlMatch`) | +| `salary` | `cipherstash.DoubleOrd()` | `public.eql_v3_double_ord` | equality + order/range | +| `accountId` | `cipherstash.BigIntOrd()` | `public.eql_v3_bigint_ord` | equality + order/range (true `bigint`) | +| `birthday` | `cipherstash.DateOrd()` | `public.eql_v3_date_ord` | equality + order/range | +| `emailVerified` | `cipherstash.Boolean()` | `public.eql_v3_boolean` | storage-only (no operators) | +| `preferences` | `cipherstash.Json()` | `public.eql_v3_json` | `eqlJsonContains` (`@>`) | 📖 See the [Prisma Next encryption docs](https://cipherstash.com/docs/stack/cipherstash/encryption/prisma-next) for the full operator reference, security model, and known limitations. @@ -22,7 +22,7 @@ A single `User` model with one column per plaintext family, exercised end-to-end | `docker-compose.yml` | Local Postgres 16 on port 54338. | | `prisma/schema.prisma` | Application schema (one `User` model exercising six cipherstash v3 domains). | | `prisma-next.config.ts` | Wires `cipherstash` into `extensionPacks`. | -| `src/db.ts` | One-call setup via `cipherstashFromStackV3({ contractJson })`. | +| `src/db.ts` | One-call setup via `cipherstashFromStack({ contractJson })`. | | `src/index.ts` | The demo flow. | | `src/prisma/contract.*` | Emitted by `pnpm emit`. | | `migrations/` | Emitted by `pnpm migration:plan` (app space + the cipherstash EQL bundle baselines). | diff --git a/examples/prisma/prisma/schema.prisma b/examples/prisma/prisma/schema.prisma index 349519b8..5d28190c 100644 --- a/examples/prisma/prisma/schema.prisma +++ b/examples/prisma/prisma/schema.prisma @@ -36,12 +36,12 @@ model User { id String @id - email cipherstash.EncryptedTextSearch() - salary cipherstash.EncryptedDoubleOrd() - accountId cipherstash.EncryptedBigIntOrd() @map("accountid") - birthday cipherstash.EncryptedDateOrd() - emailVerified cipherstash.EncryptedBoolean() @map("emailverified") - preferences cipherstash.EncryptedJson() + email cipherstash.TextSearch() + salary cipherstash.DoubleOrd() + accountId cipherstash.BigIntOrd() @map("accountid") + birthday cipherstash.DateOrd() + emailVerified cipherstash.Boolean() @map("emailverified") + preferences cipherstash.Json() @@map("users") } diff --git a/examples/prisma/src/db.ts b/examples/prisma/src/db.ts index 2f4668b9..e34ba201 100644 --- a/examples/prisma/src/db.ts +++ b/examples/prisma/src/db.ts @@ -2,7 +2,7 @@ * Wire the Prisma Next Postgres runtime with the cipherstash EQL v3 * extension in one call. * - * `cipherstashFromStackV3({ contractJson })` derives the v3 encryption + * `cipherstashFromStack({ contractJson })` derives the v3 encryption * schemas from the contract (one `public.eql_v3_*` domain per column), * constructs the `@cipherstash/stack` `EncryptionV3` client against * your `CS_*` env vars or local profile, builds the SDK adapter, and @@ -11,19 +11,19 @@ * does not model. * * A v3 client is v3-only: a contract carrying v2 cipherstash codec ids - * is rejected at setup (use `cipherstashFromStack` from + * is rejected at setup (use `cipherstashFromStackV2` from * `@cipherstash/prisma-next/stack` for a v2 contract). */ import 'dotenv/config' -import { cipherstashFromStackV3 } from '@cipherstash/prisma-next/v3' +import { cipherstashFromStack } from '@cipherstash/prisma-next/v3' import postgres from '@prisma-next/postgres/runtime' import type { Contract } from './prisma/contract.d' import contractJson from './prisma/contract.json' with { type: 'json' } -const cipherstash = await cipherstashFromStackV3({ contractJson }) +const cipherstash = await cipherstashFromStack({ contractJson }) export const db = postgres({ contractJson, diff --git a/examples/prisma/test/e2e/mixed.e2e.test.ts b/examples/prisma/test/e2e/mixed.e2e.test.ts index 95776d32..4f361c2d 100644 --- a/examples/prisma/test/e2e/mixed.e2e.test.ts +++ b/examples/prisma/test/e2e/mixed.e2e.test.ts @@ -13,7 +13,7 @@ * middleware controls and what this suite observes.) * * Crossing counts are observed by wrapping a fresh `CipherstashSdk` - * (built from `cipherstashFromStackV3({ contractJson }).encryptionClient` + * (built from `cipherstashFromStack({ contractJson }).encryptionClient` * via `createCipherstashV3Sdk`) with a counting decorator and threading * the wrapped instance into a private `db` runtime. Concretely: * @@ -40,7 +40,7 @@ import { eqlAsc, } from '@cipherstash/prisma-next/runtime' import { - cipherstashFromStackV3, + cipherstashFromStack, createCipherstashV3Sdk, deriveStackSchemasV3, } from '@cipherstash/prisma-next/v3' @@ -141,12 +141,12 @@ describe('Mixed-domain e2e (live PG + EQL v3 + ZeroKMS)', () => { let runtime: { close(): Promise } | undefined beforeAll(async () => { - // Reuse the encryption client from `cipherstashFromStackV3` so the + // Reuse the encryption client from `cipherstashFromStack` so the // counting wrapper observes the same ZeroKMS workspace + schema // surface the example app would in production. Re-derive the v3 // stack schemas from `contractJson` to satisfy // `createCipherstashV3Sdk`'s `(client, schemas)` contract. - const { encryptionClient } = await cipherstashFromStackV3({ contractJson }) + const { encryptionClient } = await cipherstashFromStack({ contractJson }) const schemas = deriveStackSchemasV3(contractJson) const baseSdk = createCipherstashV3Sdk(encryptionClient, schemas) counting = wrapWithCounting(baseSdk) diff --git a/examples/prisma/test/e2e/str-range.e2e.test.ts b/examples/prisma/test/e2e/str-range.e2e.test.ts index 4c4703ec..bc1b7779 100644 --- a/examples/prisma/test/e2e/str-range.e2e.test.ts +++ b/examples/prisma/test/e2e/str-range.e2e.test.ts @@ -2,7 +2,7 @@ * End-to-end coverage for the `eql_v3_text_search` domain against * live Postgres + EQL v3 + ZeroKMS. * - * `cipherstash.EncryptedTextSearch()` is the maximal text domain: + * `cipherstash.TextSearch()` is the maximal text domain: * equality + order/range (OPE) + free-text search (bloom match) on one * column. Pins: * - Round-trip decrypt recovers the source strings. diff --git a/packages/prisma-next/README.md b/packages/prisma-next/README.md index 9bf21482..a8c7eb8e 100644 --- a/packages/prisma-next/README.md +++ b/packages/prisma-next/README.md @@ -12,7 +12,7 @@ Declare encrypted columns directly in `schema.prisma`, and the framework's migra - 🔍 Searchable encryption — equality, free-text search, range, order, JSON path and containment - 🎯 Type-safe query operators — EQL v3 uses the EQL-derived `eql*` vocabulary (`eqlEq`, `eqlMatch`, `eqlGt`, `eqlAsc`, …); the legacy v2 surface keeps its `cipherstash*` names - ⚡ Bulk encrypt / bulk decrypt coalescing — one SDK round-trip per `(table, column)` group per query -- 🧩 One-call setup via `cipherstashFromStackV3({ contractJson })` (v2: `cipherstashFromStack`) — no duplicate stack schema to maintain +- 🧩 One-call setup via `cipherstashFromStack({ contractJson })` (v2: `cipherstashFromStackV2`) — no duplicate stack schema to maintain - 🛡️ Plaintext redaction on every implicit serialisation path (`toJSON`, `toString`, `util.inspect`, …) ## Installation @@ -27,10 +27,10 @@ npm install @cipherstash/stack @cipherstash/prisma-next // prisma/schema.prisma model User { id String @id - email cipherstash.EncryptedString() - salary cipherstash.EncryptedDouble() - birthday cipherstash.EncryptedDate() - preferences cipherstash.EncryptedJson() + email cipherstash.TextSearch() + salary cipherstash.DoubleOrd() + birthday cipherstash.DateOrd() + preferences cipherstash.Json() } ``` @@ -47,12 +47,12 @@ export default defineConfig({ ```typescript // src/db.ts import "dotenv/config" -import { cipherstashFromStack } from "@cipherstash/prisma-next/stack" +import { cipherstashFromStackV2 } from "@cipherstash/prisma-next/stack" import postgres from "@prisma-next/postgres/runtime" import type { Contract } from "./prisma/contract.d" import contractJson from "./prisma/contract.json" with { type: "json" } -const cipherstash = await cipherstashFromStack({ contractJson }) +const cipherstash = await cipherstashFromStackV2({ contractJson }) export const db = postgres({ contractJson, @@ -91,8 +91,8 @@ See the [full documentation](https://cipherstash.com/docs/stack/cipherstash/encr | Subpath | Purpose | | ---------------- | ------------------------------------------------------------------------------------------------------ | -| `./v3` | The complete EQL v3 surface: `cipherstashFromStackV3`, the `eql*` query operations, `eqlAsc`/`eqlDesc`, envelopes, middleware, SDK adapter | -| `./stack` | One-call setup against `@cipherstash/stack` (EQL v2): `cipherstashFromStack`, `deriveStackSchemas`, `createCipherstashSdk` | +| `./v3` | The complete EQL v3 surface: `cipherstashFromStack`, the `eql*` query operations, `eqlAsc`/`eqlDesc`, envelopes, middleware, SDK adapter | +| `./stack` | One-call setup against `@cipherstash/stack` (EQL v2): `cipherstashFromStackV2`, `deriveStackSchemas`, `createCipherstashSdk` | | `./control` | `SqlControlExtensionDescriptor` (contract space + pack meta + codec lifecycle hooks) | | `./runtime` | Six envelope classes + `CipherstashSdk` + codec runtime + `decryptAll` + four free-standing helpers | | `./middleware` | `bulkEncryptMiddleware(sdk)` | diff --git a/packages/prisma-next/src/contract-authoring.ts b/packages/prisma-next/src/contract-authoring.ts index 6312ad2a..7d31afa1 100644 --- a/packages/prisma-next/src/contract-authoring.ts +++ b/packages/prisma-next/src/contract-authoring.ts @@ -7,14 +7,14 @@ * `*_ord_ore` variants) gets exactly one argument-less PSL constructor, * derived 1:1 from `EXPOSED_DOMAIN_ENTRIES`: * - * `cipherstash.EncryptedTextSearch` → codec + * `cipherstash.TextSearch` → codec * `cipherstash/eql-v3/eql_v3_text_search@1`, native type * `public.eql_v3_text_search`, static * `typeParams: { castAs, capabilities }`. * * There are NO options: the constructor IS the capability set. The name is * the mechanical `Encrypted` transform of the bare domain with - * its `eql_v3_` prefix stripped (`eql_v3_bigint_ord` → `EncryptedBigIntOrd`); + * its `eql_v3_` prefix stripped (`eql_v3_bigint_ord` → `BigIntOrd`); * the codec id keeps the registry key VERBATIM (never name-transformed). * * ### Static typeParams — confirmed framework shape (Step 0) @@ -107,13 +107,29 @@ function coreName(bareDomain: string): string { return `${stemLabel}${suffixLabel}` } -/** `eql_v3_text_search` → `EncryptedTextSearch`, `eql_v3_bigint_ord` → `EncryptedBigIntOrd`. */ +/** + * `eql_v3_text_search` → `TextSearch`, `eql_v3_bigint_ord` → `BigIntOrd`. + * + * The `cipherstash.` PSL namespace already disambiguates, so the constructor + * names drop the `Encrypted` prefix to line up with the stack / Drizzle + * `types.*` catalog (`types.TextSearch`, `types.BigintOrd`, …). The v2 + * constructors keep their `*V2` names; the runtime envelope classes + * (`EncryptedString`, `EncryptedBoolean`, …) are a separate surface and are + * unchanged. + */ export function v3PascalName(bareDomain: string): string { - return `Encrypted${coreName(bareDomain)}` + return coreName(bareDomain) } -/** `eql_v3_text_search` → `encryptedTextSearch`, `eql_v3_bigint_ord` → `encryptedBigIntOrd`. */ +/** + * `eql_v3_text_search` → `textSearch`, `eql_v3_bigint_ord` → `bigIntOrd`. + * + * Kept in lockstep with {@link v3PascalName} — the camelCase TS-authoring + * factory name is the PSL constructor name with a lowercased first letter + * (a property test enforces the two agree modulo first-letter case). + */ export function v3CamelName(bareDomain: string): string { - return `encrypted${coreName(bareDomain)}` + const core = coreName(bareDomain) + return `${core.charAt(0).toLowerCase()}${core.slice(1)}` } /** diff --git a/packages/prisma-next/src/exports/column-types.ts b/packages/prisma-next/src/exports/column-types.ts index dfb152d3..8968f943 100644 --- a/packages/prisma-next/src/exports/column-types.ts +++ b/packages/prisma-next/src/exports/column-types.ts @@ -10,7 +10,7 @@ * `{ castAs, capabilities }` typeParams block — identical to what its PSL * counterpart lowers to, so PSL- and TS-authored contracts emit * byte-identical `contract.json`. There are NO options: the factory IS the - * capability set (`encryptedTextSearch` vs `encryptedText`, and so on). + * capability set (`textSearch` vs `text`, and so on). * * ## v2: legacy `*V2` aliases * @@ -120,50 +120,50 @@ function v3Authored( } // Each factory has a DISTINCT return type (V3ColumnDescriptor); -// `encryptedBigIntOrd()` is not assignable to `encryptedText()` and vice-versa. +// `bigIntOrd()` is not assignable to `text()` and vice-versa. // Note the stack factory names: bigint is `types.Bigint*` (not `BigInt*`). // text family -export const encryptedText = v3Authored(types.Text) -export const encryptedTextEq = v3Authored(types.TextEq) -export const encryptedTextOrd = v3Authored(types.TextOrd) -export const encryptedTextMatch = v3Authored(types.TextMatch) -export const encryptedTextSearch = v3Authored(types.TextSearch) +export const text = v3Authored(types.Text) +export const textEq = v3Authored(types.TextEq) +export const textOrd = v3Authored(types.TextOrd) +export const textMatch = v3Authored(types.TextMatch) +export const textSearch = v3Authored(types.TextSearch) // integer -export const encryptedInteger = v3Authored(types.Integer) -export const encryptedIntegerEq = v3Authored(types.IntegerEq) -export const encryptedIntegerOrd = v3Authored(types.IntegerOrd) +export const integer = v3Authored(types.Integer) +export const integerEq = v3Authored(types.IntegerEq) +export const integerOrd = v3Authored(types.IntegerOrd) // smallint -export const encryptedSmallint = v3Authored(types.Smallint) -export const encryptedSmallintEq = v3Authored(types.SmallintEq) -export const encryptedSmallintOrd = v3Authored(types.SmallintOrd) +export const smallint = v3Authored(types.Smallint) +export const smallintEq = v3Authored(types.SmallintEq) +export const smallintOrd = v3Authored(types.SmallintOrd) // bigint -export const encryptedBigInt = v3Authored(types.Bigint) -export const encryptedBigIntEq = v3Authored(types.BigintEq) -export const encryptedBigIntOrd = v3Authored(types.BigintOrd) +export const bigInt = v3Authored(types.Bigint) +export const bigIntEq = v3Authored(types.BigintEq) +export const bigIntOrd = v3Authored(types.BigintOrd) // numeric -export const encryptedNumeric = v3Authored(types.Numeric) -export const encryptedNumericEq = v3Authored(types.NumericEq) -export const encryptedNumericOrd = v3Authored(types.NumericOrd) +export const numeric = v3Authored(types.Numeric) +export const numericEq = v3Authored(types.NumericEq) +export const numericOrd = v3Authored(types.NumericOrd) // real -export const encryptedReal = v3Authored(types.Real) -export const encryptedRealEq = v3Authored(types.RealEq) -export const encryptedRealOrd = v3Authored(types.RealOrd) +export const real = v3Authored(types.Real) +export const realEq = v3Authored(types.RealEq) +export const realOrd = v3Authored(types.RealOrd) // double -export const encryptedDouble = v3Authored(types.Double) -export const encryptedDoubleEq = v3Authored(types.DoubleEq) -export const encryptedDoubleOrd = v3Authored(types.DoubleOrd) +export const double = v3Authored(types.Double) +export const doubleEq = v3Authored(types.DoubleEq) +export const doubleOrd = v3Authored(types.DoubleOrd) // date -export const encryptedDate = v3Authored(types.Date) -export const encryptedDateEq = v3Authored(types.DateEq) -export const encryptedDateOrd = v3Authored(types.DateOrd) +export const date = v3Authored(types.Date) +export const dateEq = v3Authored(types.DateEq) +export const dateOrd = v3Authored(types.DateOrd) // timestamp -export const encryptedTimestamp = v3Authored(types.Timestamp) -export const encryptedTimestampEq = v3Authored(types.TimestampEq) -export const encryptedTimestampOrd = v3Authored(types.TimestampOrd) +export const timestamp = v3Authored(types.Timestamp) +export const timestampEq = v3Authored(types.TimestampEq) +export const timestampOrd = v3Authored(types.TimestampOrd) // boolean (storage-only) -export const encryptedBoolean = v3Authored(types.Boolean) +export const boolean = v3Authored(types.Boolean) // json (encrypted JSONB, ste_vec containment) -export const encryptedJson = v3Authored(types.Json) +export const json = v3Authored(types.Json) // --------------------------------------------------------------------------- // v2 legacy aliases (verbatim pre-rename bodies, now named *V2) diff --git a/packages/prisma-next/src/exports/stack.ts b/packages/prisma-next/src/exports/stack.ts index d60fcbba..8ed398f8 100644 --- a/packages/prisma-next/src/exports/stack.ts +++ b/packages/prisma-next/src/exports/stack.ts @@ -3,7 +3,7 @@ * `@cipherstash/stack` SDK against a Prisma Next contract. * * The three exports here form a layered API. Most consumers want - * {@link cipherstashFromStack}; the two primitives are exposed for + * {@link cipherstashFromStackV2}; the two primitives are exposed for * advanced users who need to interpose custom logic. * * - {@link deriveStackSchemas} — pure function, contract.json → @@ -15,7 +15,7 @@ * shape. Use when you've constructed the client yourself (custom * keyset, multi-tenant routing). * - * - {@link cipherstashFromStack} — the all-in-one factory. + * - {@link cipherstashFromStackV2} — the all-in-one factory. * Returns ready-to-spread arrays for `postgres({...})`. * * This subpath imports `@cipherstash/stack` directly. Consumers who @@ -31,17 +31,17 @@ export type { CipherstashFromStackOptions, CipherstashFromStackResult, } from '../stack/from-stack' -export { cipherstashFromStack } from '../stack/from-stack' +export { cipherstashFromStackV2 } from '../stack/from-stack' // --------------------------------------------------------------------------- // EQL v3 (decision 1b: a SEPARATE entry point — a client is v2 or v3, -// never both; `cipherstashFromStackV3` rejects contracts carrying v2 +// never both; `cipherstashFromStack` rejects contracts carrying v2 // cipherstash codec ids). // --------------------------------------------------------------------------- export type { CipherstashFromStackV3Options, CipherstashFromStackV3Result, } from '../stack/from-stack-v3' -export { cipherstashFromStackV3 } from '../stack/from-stack-v3' +export { cipherstashFromStack } from '../stack/from-stack-v3' export { createCipherstashSdk } from '../stack/sdk-adapter' export type { V3ContractColumnEntry, diff --git a/packages/prisma-next/src/exports/v3.ts b/packages/prisma-next/src/exports/v3.ts index c829acc6..4dc3160c 100644 --- a/packages/prisma-next/src/exports/v3.ts +++ b/packages/prisma-next/src/exports/v3.ts @@ -6,7 +6,7 @@ * * Most consumers want exactly one symbol from here: * - * const cipherstash = await cipherstashFromStackV3({ contractJson }) + * const cipherstash = await cipherstashFromStack({ contractJson }) * * The remaining exports are the primitives that factory composes — * schema derivation, the SDK adapter, the runtime descriptor, the @@ -14,7 +14,7 @@ * advanced users interposing custom logic (multi-tenant SDK routing, * a non-stack `CipherstashSdk` implementation, …). * - * Bundle note: this entry re-exports `cipherstashFromStackV3`, which + * Bundle note: this entry re-exports `cipherstashFromStack`, which * imports `@cipherstash/stack`. Consumers who need an SDK-free runtime * plane (custom `CipherstashSdk`) should import from `./runtime` * instead — its v3 exports carry no `@cipherstash/stack` client @@ -34,7 +34,7 @@ export type { CipherstashFromStackV3Options, CipherstashFromStackV3Result, } from '../stack/from-stack-v3' -export { cipherstashFromStackV3 } from '../stack/from-stack-v3' +export { cipherstashFromStack } from '../stack/from-stack-v3' // User-facing value envelopes (version-neutral classes + the v3-only // EncryptedNumber). export * from '../v3/barrel' diff --git a/packages/prisma-next/src/stack/from-stack-v3.ts b/packages/prisma-next/src/stack/from-stack-v3.ts index 48e95864..9db3ef1e 100644 --- a/packages/prisma-next/src/stack/from-stack-v3.ts +++ b/packages/prisma-next/src/stack/from-stack-v3.ts @@ -3,9 +3,9 @@ * `@cipherstash/stack` EQL v3 client — the v3-only sibling of * `./from-stack.ts` (decision 1b: v2 and v3 are SEPARATE entry points * that are never co-registered in one client; the v2 - * `cipherstashFromStack` is untouched by this module). + * `cipherstashFromStackV2` is untouched by this module). * - * const cipherstash = await cipherstashFromStackV3({ contractJson }) + * const cipherstash = await cipherstashFromStack({ contractJson }) * * const db = postgres({ * contractJson, @@ -73,14 +73,14 @@ export interface CipherstashFromStackV3Result { readonly encryptionClient: TypedEncryptionClient } -export async function cipherstashFromStackV3( +export async function cipherstashFromStack( opts: CipherstashFromStackV3Options, ): Promise { const foreignIds = collectNonV3CipherstashCodecIds(opts.contractJson) if (foreignIds.length > 0) { throw new Error( - `cipherstashFromStackV3: contract.json contains non-v3 cipherstash codec ids [${foreignIds.join(', ')}]. ` + - 'A v3 client is v3-only; use cipherstashFromStack for a v2 contract. ' + + `cipherstashFromStack: contract.json contains non-v3 cipherstash codec ids [${foreignIds.join(', ')}]. ` + + 'A v3 client is v3-only; use cipherstashFromStackV2 for a v2 contract. ' + 'Mixed v2+v3 cipherstash columns in one client are unsupported.', ) } @@ -88,9 +88,9 @@ export async function cipherstashFromStackV3( const derived = deriveStackSchemasV3(opts.contractJson) if (derived.length === 0) { throw new Error( - 'cipherstashFromStackV3: no v3 cipherstash columns found in contract.json. ' + + 'cipherstashFromStack: no v3 cipherstash columns found in contract.json. ' + 'Declare at least one v3 `cipherstash.Encrypted*()` column in prisma/schema.prisma and re-emit the contract ' + - '(or use cipherstashFromStack if this is a v2 contract).', + '(or use cipherstashFromStackV2 if this is a v2 contract).', ) } diff --git a/packages/prisma-next/src/stack/from-stack.ts b/packages/prisma-next/src/stack/from-stack.ts index 10ec6c3f..0ed57a57 100644 --- a/packages/prisma-next/src/stack/from-stack.ts +++ b/packages/prisma-next/src/stack/from-stack.ts @@ -8,7 +8,7 @@ * with a single async factory that returns ready-to-spread arrays for * `postgres({...})`: * - * const cipherstash = await cipherstashFromStack({ contractJson }) + * const cipherstash = await cipherstashFromStackV2({ contractJson }) * * const db = postgres({ * contractJson, @@ -24,7 +24,7 @@ * bundle's installed configuration disagrees with. * * This is the **EQL v2** entry point. For an EQL v3 contract - * (`cipherstash/eql-v3/*` codec ids) use `cipherstashFromStackV3` + * (`cipherstash/eql-v3/*` codec ids) use `cipherstashFromStack` * (`./from-stack-v3.ts`) — v2 and v3 are separate entry points that * are never co-registered in one client (decision 1b). */ @@ -72,7 +72,7 @@ export interface CipherstashFromStackResult { readonly encryptionClient: EncryptionClient } -export async function cipherstashFromStack( +export async function cipherstashFromStackV2( opts: CipherstashFromStackOptions, ): Promise { const derived = deriveStackSchemas(opts.contractJson) @@ -80,7 +80,7 @@ export async function cipherstashFromStack( const [first, ...rest] = schemas if (first === undefined) { throw new Error( - 'cipherstashFromStack: no cipherstash columns found in contract.json AND no override `schemas` supplied. ' + + 'cipherstashFromStackV2: no cipherstash columns found in contract.json AND no override `schemas` supplied. ' + "`@cipherstash/stack`'s `Encryption({ schemas })` requires at least one `EncryptedTable`. " + 'Check that prisma/schema.prisma declares at least one `cipherstash.Encrypted*()` column and that ' + '`pnpm emit` has been run since the last edit.', @@ -188,7 +188,7 @@ function divergence( hint: string, ): never { throw new Error( - `cipherstashFromStack: schema divergence on ${loc}. Contract ${contractSide} but override ${overrideSide}. ${hint}`, + `cipherstashFromStackV2: schema divergence on ${loc}. Contract ${contractSide} but override ${overrideSide}. ${hint}`, ) } diff --git a/packages/prisma-next/src/v3/derive-schemas-v3.ts b/packages/prisma-next/src/v3/derive-schemas-v3.ts index 4b93ed7f..a4df3127 100644 --- a/packages/prisma-next/src/v3/derive-schemas-v3.ts +++ b/packages/prisma-next/src/v3/derive-schemas-v3.ts @@ -85,7 +85,7 @@ export interface V3ContractColumnEntry { /** * Flatten every contract column across all namespaces/tables. Shared by - * {@link deriveStackSchemasV3} and `cipherstashFromStackV3`'s + * {@link deriveStackSchemasV3} and `cipherstashFromStack`'s * v2-codec-id scan (a v3 client is v3-only; foreign cipherstash codec * ids are a hard error there). */ diff --git a/packages/prisma-next/src/v3/from-stack-v3-validate.ts b/packages/prisma-next/src/v3/from-stack-v3-validate.ts index 7816a06e..07b75a7d 100644 --- a/packages/prisma-next/src/v3/from-stack-v3-validate.ts +++ b/packages/prisma-next/src/v3/from-stack-v3-validate.ts @@ -1,5 +1,5 @@ /** - * v3 override validation for `cipherstashFromStackV3`. + * v3 override validation for `cipherstashFromStack`. * * Compares EXACT domain identity (`getEqlType()` → `public.eql_v3_*`), * not the v2 rule of cast_as + installed-index-set equivalence: @@ -32,7 +32,7 @@ export function assertV3SchemasAgree( const overrideDomain = overrideDomains.get(column) if (derivedDomain !== overrideDomain) { throw new Error( - `cipherstashFromStackV3: schema divergence on column "${derived.tableName}"."${column}". ` + + `cipherstashFromStack: schema divergence on column "${derived.tableName}"."${column}". ` + `Contract domain "${derivedDomain ?? '(missing)'}" but override domain "${overrideDomain ?? '(missing)'}". ` + 'Overrides must match the contract exactly on contract-declared tables — ' + 'fix prisma/schema.prisma and re-emit rather than overriding.', diff --git a/packages/prisma-next/test/authoring.test.ts b/packages/prisma-next/test/authoring.test.ts index f83b7842..a6b437e5 100644 --- a/packages/prisma-next/test/authoring.test.ts +++ b/packages/prisma-next/test/authoring.test.ts @@ -5,7 +5,7 @@ * - Every exposed v3 domain (the catalog minus `*_ord_ore`) gets exactly * one concrete, argument-less `typeConstructor` whose name is the * mechanical `Encrypted` transform of the bare domain - * (`eql_v3_text_search` → `EncryptedTextSearch`). The output carries the + * (`eql_v3_text_search` → `TextSearch`). The output carries the * domain's STATIC codec id, `public.eql_v3_*` native type, and a static * `{ castAs, capabilities }` typeParams block — no options, no * `AuthoringArgRef` nodes. @@ -14,7 +14,7 @@ * shapes, same `cipherstash/*@1` codec ids, same `eql_v2_encrypted` * native type, same `true`-defaulting `AuthoringArgRef` typeParams. * - `EncryptedString` (unqualified) no longer exists — v3 text columns use - * the `EncryptedText*` family. `EncryptedJson` (unqualified) is now the + * the `Text*` family. `Json` (unqualified) is now the * v3 `eql_v3_json` domain. * * Full PSL→ColumnTypeDescriptor lowering is exercised in @@ -61,8 +61,8 @@ describe('cipherstash v3 authoring (concrete per-domain, static descriptors)', ( } }) - it('EncryptedTextSearch → public.eql_v3_text_search with full capabilities', () => { - expect(ns.EncryptedTextSearch?.output).toMatchObject({ + it('TextSearch → public.eql_v3_text_search with full capabilities', () => { + expect(ns.TextSearch?.output).toMatchObject({ codecId: 'cipherstash/eql-v3/eql_v3_text_search@1', nativeType: 'public.eql_v3_text_search', typeParams: { @@ -76,8 +76,8 @@ describe('cipherstash v3 authoring (concrete per-domain, static descriptors)', ( }) }) - it('EncryptedBoolean → storage-only public.eql_v3_boolean (no equality constructor exists)', () => { - expect(ns.EncryptedBoolean?.output).toMatchObject({ + it('Boolean → storage-only public.eql_v3_boolean (no equality constructor exists)', () => { + expect(ns.Boolean?.output).toMatchObject({ codecId: 'cipherstash/eql-v3/eql_v3_boolean@1', nativeType: 'public.eql_v3_boolean', typeParams: { @@ -89,11 +89,11 @@ describe('cipherstash v3 authoring (concrete per-domain, static descriptors)', ( }, }, }) - expect(ns.EncryptedBooleanEq).toBeUndefined() + expect(ns.BooleanEq).toBeUndefined() }) - it('EncryptedJson → public.eql_v3_json with searchableJson-only capabilities', () => { - expect(ns.EncryptedJson?.output).toMatchObject({ + it('Json → public.eql_v3_json with searchableJson-only capabilities', () => { + expect(ns.Json?.output).toMatchObject({ codecId: 'cipherstash/eql-v3/eql_v3_json@1', nativeType: 'public.eql_v3_json', typeParams: { @@ -109,20 +109,20 @@ describe('cipherstash v3 authoring (concrete per-domain, static descriptors)', ( }) it('BigInt family is present (public.eql_v3_bigint*, castAs bigint); no *OrdOre, no v3 String', () => { - expect(ns.EncryptedBigInt?.output).toMatchObject({ + expect(ns.BigInt?.output).toMatchObject({ nativeType: 'public.eql_v3_bigint', typeParams: { castAs: 'bigint' }, }) - expect(ns.EncryptedBigIntEq?.output).toMatchObject({ + expect(ns.BigIntEq?.output).toMatchObject({ nativeType: 'public.eql_v3_bigint_eq', }) - expect(ns.EncryptedBigIntOrd?.output).toMatchObject({ + expect(ns.BigIntOrd?.output).toMatchObject({ nativeType: 'public.eql_v3_bigint_ord', }) - expect(ns.EncryptedBigIntOrdOre).toBeUndefined() - expect(ns.EncryptedIntegerOrdOre).toBeUndefined() - expect(ns.EncryptedTextOrdOre).toBeUndefined() - // v3 text columns use EncryptedText*; the unqualified v2 name is gone. + expect(ns.BigIntOrdOre).toBeUndefined() + expect(ns.IntegerOrdOre).toBeUndefined() + expect(ns.TextOrdOre).toBeUndefined() + // v3 text columns use Text*; the unqualified v2 name is gone. expect(ns.EncryptedString).toBeUndefined() }) diff --git a/packages/prisma-next/test/bundling-isolation.test.ts b/packages/prisma-next/test/bundling-isolation.test.ts index 02889e5a..51a40a46 100644 --- a/packages/prisma-next/test/bundling-isolation.test.ts +++ b/packages/prisma-next/test/bundling-isolation.test.ts @@ -167,9 +167,9 @@ const ALLOWED_SHARED_CHUNK_MARKER_SETS: ReadonlyArray = [ * planes in one module graph is exactly the confusion decision 1b * (a client is v2 or v3, never both) exists to prevent. * - * Marker choice notes: `bulkEncryptMiddleware` and `cipherstashFromStack` + * Marker choice notes: `bulkEncryptMiddleware` and `cipherstashFromStackV2` * are NOT usable as v2 markers — they are substrings of their v3 - * siblings (`bulkEncryptMiddlewareV3`, `cipherstashFromStackV3`), so a + * siblings (`bulkEncryptMiddlewareV3`, `cipherstashFromStack`), so a * substring scan would false-positive on every v3 chunk. */ const V2_WIRE_MARKERS = [ diff --git a/packages/prisma-next/test/column-types.test.ts b/packages/prisma-next/test/column-types.test.ts index 9cda0696..2ffd5aad 100644 --- a/packages/prisma-next/test/column-types.test.ts +++ b/packages/prisma-next/test/column-types.test.ts @@ -40,16 +40,16 @@ describe('v3 TS factories', () => { } }) - it('encryptedBigIntOrd() emits public.eql_v3_bigint_ord with bigint castAs', () => { - expect(columnTypes.encryptedBigIntOrd()).toMatchObject({ + it('bigIntOrd() emits public.eql_v3_bigint_ord with bigint castAs', () => { + expect(columnTypes.bigIntOrd()).toMatchObject({ codecId: 'cipherstash/eql-v3/eql_v3_bigint_ord@1', nativeType: 'public.eql_v3_bigint_ord', typeParams: { castAs: 'bigint' }, }) }) - it('encryptedTextSearch() emits public.eql_v3_text_search with full capabilities', () => { - expect(columnTypes.encryptedTextSearch()).toEqual({ + it('textSearch() emits public.eql_v3_text_search with full capabilities', () => { + expect(columnTypes.textSearch()).toEqual({ codecId: 'cipherstash/eql-v3/eql_v3_text_search@1', nativeType: 'public.eql_v3_text_search', typeParams: { @@ -63,8 +63,8 @@ describe('v3 TS factories', () => { }) }) - it('encryptedJson() emits public.eql_v3_json with searchableJson-only capabilities', () => { - expect(columnTypes.encryptedJson()).toEqual({ + it('json() emits public.eql_v3_json with searchableJson-only capabilities', () => { + expect(columnTypes.json()).toEqual({ codecId: 'cipherstash/eql-v3/eql_v3_json@1', nativeType: 'public.eql_v3_json', typeParams: { @@ -84,8 +84,8 @@ describe('v3 TS factories', () => { // instance would make one caller's mutation alter every later // contract (call-order-dependent output). `v3Authored` shallow-copies // the capabilities block per call, pinned here. - const first = columnTypes.encryptedTextSearch() - const second = columnTypes.encryptedTextSearch() + const first = columnTypes.textSearch() + const second = columnTypes.textSearch() expect(second).not.toBe(first) expect(second.typeParams.capabilities).not.toBe( first.typeParams.capabilities, @@ -106,18 +106,19 @@ describe('v3 TS factories', () => { orderAndRange: true, freeTextSearch: true, }) - expect(columnTypes.encryptedTextSearch().typeParams.capabilities).toEqual({ + expect(columnTypes.textSearch().typeParams.capabilities).toEqual({ equality: true, orderAndRange: true, freeTextSearch: true, }) }) - it('exposes no *OrdOre factories and no v3 encryptedString', () => { + it('exposes no *OrdOre factories and no v3 string factory', () => { const exported = columnTypes as Record - expect(exported['encryptedBigIntOrdOre']).toBeUndefined() - expect(exported['encryptedTextOrdOre']).toBeUndefined() - expect(exported['encryptedString']).toBeUndefined() + expect(exported['bigIntOrdOre']).toBeUndefined() + expect(exported['textOrdOre']).toBeUndefined() + // v3 text uses `text`/`textEq`/`textSearch`/… — there is no `string`. + expect(exported['string']).toBeUndefined() }) }) diff --git a/packages/prisma-next/test/from-stack-divergence.test.ts b/packages/prisma-next/test/from-stack-divergence.test.ts index 42c34316..565059d3 100644 --- a/packages/prisma-next/test/from-stack-divergence.test.ts +++ b/packages/prisma-next/test/from-stack-divergence.test.ts @@ -1,10 +1,10 @@ /** - * Pin the divergence-check semantics of `cipherstashFromStack`. + * Pin the divergence-check semantics of `cipherstashFromStackV2`. * - * The full `cipherstashFromStack` path is not exercisable in unit + * The full `cipherstashFromStackV2` path is not exercisable in unit * tests because it calls `Encryption({ schemas })` which talks to * ZeroKMS at module-evaluation time. We instead pull out the - * divergence check by calling `cipherstashFromStack` with an + * divergence check by calling `cipherstashFromStackV2` with an * intentionally-broken override; the assertion fires before any * SDK round-trip is attempted, so the test stays hermetic. * @@ -19,7 +19,7 @@ import { CIPHERSTASH_BOOLEAN_CODEC_ID, CIPHERSTASH_STRING_CODEC_ID, } from '../src/extension-metadata/constants' -import { cipherstashFromStack } from '../src/stack/from-stack' +import { cipherstashFromStackV2 } from '../src/stack/from-stack' function makeContract() { return { @@ -48,7 +48,7 @@ function makeContract() { } } -describe('cipherstashFromStack — divergence check', () => { +describe('cipherstashFromStackV2 — divergence check', () => { it('throws when an override drops a column the contract declares', async () => { const override = encryptedTable('users', { email: encryptedColumn('email').equality().freeTextSearch(), @@ -56,7 +56,7 @@ describe('cipherstashFromStack — divergence check', () => { }) await expect( - cipherstashFromStack({ + cipherstashFromStackV2({ contractJson: makeContract(), schemas: [override], }), @@ -71,7 +71,7 @@ describe('cipherstashFromStack — divergence check', () => { }) await expect( - cipherstashFromStack({ + cipherstashFromStackV2({ contractJson: makeContract(), schemas: [override], }), @@ -85,7 +85,7 @@ describe('cipherstashFromStack — divergence check', () => { }) await expect( - cipherstashFromStack({ + cipherstashFromStackV2({ contractJson: makeContract(), schemas: [override], }), @@ -102,7 +102,7 @@ describe('cipherstashFromStack — divergence check', () => { }) await expect( - cipherstashFromStack({ + cipherstashFromStackV2({ contractJson: makeContract(), schemas: [override], }), @@ -118,7 +118,7 @@ describe('cipherstashFromStack — divergence check', () => { }, } await expect( - cipherstashFromStack({ contractJson: emptyContract }), + cipherstashFromStackV2({ contractJson: emptyContract }), ).rejects.toThrow(/no cipherstash columns found/) }) }) diff --git a/packages/prisma-next/test/live/bulk-encrypt-live-pg.test.ts b/packages/prisma-next/test/live/bulk-encrypt-live-pg.test.ts index 8cebeb22..a2b34e81 100644 --- a/packages/prisma-next/test/live/bulk-encrypt-live-pg.test.ts +++ b/packages/prisma-next/test/live/bulk-encrypt-live-pg.test.ts @@ -1,6 +1,6 @@ /** * Live-PG bulk-encrypt middleware: the REAL `bulkEncryptMiddlewareV3` - * instance wired by `cipherstashFromStackV3` (real `EncryptionV3` + * instance wired by `cipherstashFromStack` (real `EncryptionV3` * client, not a fake) drives a multi-row INSERT. Proves end-to-end: * the AST walk stamps `(table, column)` routing keys, the per-column * batch makes ONE bulkEncrypt crossing, the param slots are replaced diff --git a/packages/prisma-next/test/live/helpers/harness.ts b/packages/prisma-next/test/live/helpers/harness.ts index 15e62ad4..08489527 100644 --- a/packages/prisma-next/test/live/helpers/harness.ts +++ b/packages/prisma-next/test/live/helpers/harness.ts @@ -3,7 +3,7 @@ * * Everything on the encryption path is REAL: * - * - the client is `cipherstashFromStackV3({ contractJson })` over a + * - the client is `cipherstashFromStack({ contractJson })` over a * real `EncryptionV3` (ZeroKMS round-trips, no fakes); * - writes go through the REAL v3 bulk-encrypt middleware returned by * that factory (`cs.middleware[0].beforeExecute`), driven exactly @@ -42,7 +42,7 @@ import type { EncryptedEnvelopeBase } from '../../../src/execution/envelope-base import type { CipherstashSdk } from '../../../src/execution/sdk' import { type CipherstashFromStackV3Result, - cipherstashFromStackV3, + cipherstashFromStack, } from '../../../src/stack/from-stack-v3' import { toV3CodecId, @@ -162,7 +162,7 @@ export async function createLiveTable( export interface LiveV3Client { readonly cs: CipherstashFromStackV3Result - /** The REAL middleware instance wired by `cipherstashFromStackV3`. */ + /** The REAL middleware instance wired by `cipherstashFromStack`. */ readonly middleware: SqlMiddleware /** Read-side SDK adapter over the same client (envelope decrypt path). */ readonly sdk: CipherstashSdk @@ -171,10 +171,10 @@ export interface LiveV3Client { export async function setupLiveV3( contract: PostgresContract, ): Promise { - const cs = await cipherstashFromStackV3({ contractJson: contract }) + const cs = await cipherstashFromStack({ contractJson: contract }) const middleware = cs.middleware[0] if (!middleware) { - throw new Error('cipherstashFromStackV3 returned no middleware') + throw new Error('cipherstashFromStack returned no middleware') } const sdk = createCipherstashV3Sdk( cs.encryptionClient, diff --git a/packages/prisma-next/test/live/side-by-side-clients-live-pg.test.ts b/packages/prisma-next/test/live/side-by-side-clients-live-pg.test.ts index a0b6c4f3..ec1f451c 100644 --- a/packages/prisma-next/test/live/side-by-side-clients-live-pg.test.ts +++ b/packages/prisma-next/test/live/side-by-side-clients-live-pg.test.ts @@ -1,7 +1,7 @@ /** - * Live-PG v2/v3 coexistence: a v2 client (`cipherstashFromStack`, v2 + * Live-PG v2/v3 coexistence: a v2 client (`cipherstashFromStackV2`, v2 * contract, `public.eql_v2_encrypted` composite wire) and a v3 client - * (`cipherstashFromStackV3`, v3 contract, plain-JSONB domain wire) + * (`cipherstashFromStack`, v3 contract, plain-JSONB domain wire) * running in the SAME process against the SAME database — each with * its own table, registry, descriptor, and middleware. Decision 1b: * the two generations never share a client; this suite proves the two @@ -29,7 +29,7 @@ import { CIPHERSTASH_V3_EXTENSION_VERSION } from '../../src/extension-metadata/c import { deriveStackSchemas } from '../../src/stack/derive-schemas' import { type CipherstashFromStackResult, - cipherstashFromStack, + cipherstashFromStackV2, } from '../../src/stack/from-stack' import { createCipherstashSdk } from '../../src/stack/sdk-adapter' import { @@ -138,14 +138,14 @@ describeLivePg('v2 and v3 clients side by side against live Postgres', () => { await sql.unsafe( `CREATE TABLE "${V2_TABLE}" (id text PRIMARY KEY, email ${EQL_V2_ENCRYPTED_TYPE})`, ) - v2 = await cipherstashFromStack({ contractJson: v2Contract }) + v2 = await cipherstashFromStackV2({ contractJson: v2Contract }) v2Sdk = createCipherstashSdk( v2.encryptionClient, deriveStackSchemas(v2Contract), ) const v2Middleware = v2.middleware[0] if (!v2Middleware) - throw new Error('cipherstashFromStack returned no middleware') + throw new Error('cipherstashFromStackV2 returned no middleware') await insertEncryptedRows(sql, v2Middleware, V2_TABLE, [ { id: 'v2-row', diff --git a/packages/prisma-next/test/psl-interpretation.test.ts b/packages/prisma-next/test/psl-interpretation.test.ts index 9c1b1fe4..64eb746e 100644 --- a/packages/prisma-next/test/psl-interpretation.test.ts +++ b/packages/prisma-next/test/psl-interpretation.test.ts @@ -376,10 +376,10 @@ model User { }) describe('PSL interpretation: v3 argument-less constructors (static typeParams)', () => { - it('lowers cipherstash.EncryptedTextSearch() to the static v3 descriptor', () => { + it('lowers cipherstash.TextSearch() to the static v3 descriptor', () => { const result = interpret(`model User { id Int @id - email cipherstash.EncryptedTextSearch() + email cipherstash.TextSearch() } `) expect(result.ok).toBe(true) @@ -403,10 +403,10 @@ describe('PSL interpretation: v3 argument-less constructors (static typeParams)' ) }) - it('lowers cipherstash.EncryptedBoolean() to the storage-only v3 descriptor', () => { + it('lowers cipherstash.Boolean() to the storage-only v3 descriptor', () => { const result = interpret(`model User { id Int @id - enabled cipherstash.EncryptedBoolean() + enabled cipherstash.Boolean() } `) expect(result.ok).toBe(true) @@ -429,10 +429,10 @@ describe('PSL interpretation: v3 argument-less constructors (static typeParams)' ) }) - it('lowers cipherstash.EncryptedJson() with searchableJson-only capabilities', () => { + it('lowers cipherstash.Json() with searchableJson-only capabilities', () => { const result = interpret(`model User { id Int @id - payload cipherstash.EncryptedJson() + payload cipherstash.Json() } `) expect(result.ok).toBe(true) @@ -459,7 +459,7 @@ describe('PSL interpretation: v3 argument-less constructors (static typeParams)' it('rejects options on a v3 constructor (they take no arguments)', () => { const result = interpret(`model User { id Int @id - email cipherstash.EncryptedTextSearch({ equality: false }) + email cipherstash.TextSearch({ equality: false }) } `) expect(result.ok).toBe(false) diff --git a/packages/prisma-next/test/v3/column-types.test-d.ts b/packages/prisma-next/test/v3/column-types.test-d.ts index bb5329db..4f8bf922 100644 --- a/packages/prisma-next/test/v3/column-types.test-d.ts +++ b/packages/prisma-next/test/v3/column-types.test-d.ts @@ -9,37 +9,29 @@ import type { QueryCapabilities } from '@cipherstash/stack/eql/v3' import { expectTypeOf } from 'vitest' -import { - encryptedBigIntOrd, - encryptedText, - encryptedTextEq, -} from '../../src/exports/column-types' +import { bigIntOrd, text, textEq } from '../../src/exports/column-types' // 1. Factories are mutually non-assignable — distinct type per domain. -expectTypeOf(encryptedTextEq()).not.toMatchTypeOf(encryptedText()) -expectTypeOf(encryptedBigIntOrd()).not.toMatchTypeOf(encryptedText()) -expectTypeOf(encryptedText()).not.toMatchTypeOf(encryptedBigIntOrd()) +expectTypeOf(textEq()).not.toMatchTypeOf(text()) +expectTypeOf(bigIntOrd()).not.toMatchTypeOf(text()) +expectTypeOf(text()).not.toMatchTypeOf(bigIntOrd()) // 2. codecId is the literal union member, not `string`. expectTypeOf( - encryptedTextEq().codecId, + textEq().codecId, ).toEqualTypeOf<'cipherstash/eql-v3/eql_v3_text_eq@1'>() expectTypeOf( - encryptedBigIntOrd().codecId, + bigIntOrd().codecId, ).toEqualTypeOf<'cipherstash/eql-v3/eql_v3_bigint_ord@1'>() // 3. nativeType is the `public.eql_v3_*` literal, not `string`. -expectTypeOf( - encryptedTextEq().nativeType, -).toEqualTypeOf<'public.eql_v3_text_eq'>() -expectTypeOf( - encryptedBigIntOrd().nativeType, -).toEqualTypeOf<'public.eql_v3_bigint_ord'>() +expectTypeOf(textEq().nativeType).toEqualTypeOf<'public.eql_v3_text_eq'>() +expectTypeOf(bigIntOrd().nativeType).toEqualTypeOf<'public.eql_v3_bigint_ord'>() // 4. capabilities is (at least) the concrete QueryCapabilities, never `unknown`. // (The stack types each domain's capabilities as literal booleans, so this // also narrows further — the point is it is NOT `unknown`.) expectTypeOf( - encryptedTextEq().typeParams.capabilities, + textEq().typeParams.capabilities, ).toMatchTypeOf() -expectTypeOf(encryptedTextEq().typeParams.capabilities).not.toBeUnknown() +expectTypeOf(textEq().typeParams.capabilities).not.toBeUnknown() diff --git a/packages/prisma-next/test/v3/from-stack-v3.test.ts b/packages/prisma-next/test/v3/from-stack-v3.test.ts index e75dee6c..759906d3 100644 --- a/packages/prisma-next/test/v3/from-stack-v3.test.ts +++ b/packages/prisma-next/test/v3/from-stack-v3.test.ts @@ -1,18 +1,18 @@ /** - * `cipherstashFromStackV3` — the v3-only entry point's validation + * `cipherstashFromStack` — the v3-only entry point's validation * paths, all of which throw BEFORE any `EncryptionV3` client is * constructed (so no live CipherStash credentials are needed here; the * happy path is exercised by the live suite). * * Decision 1b pins: a v3 client is v3-only — a contract carrying a v2 * cipherstash codec id is a hard error, never a silently-ignored - * column; and the v2 `cipherstashFromStack` is a separate, untouched + * column; and the v2 `cipherstashFromStackV2` is a separate, untouched * entry point. */ import { encryptedTable, types } from '@cipherstash/stack/eql/v3' import { describe, expect, it } from 'vitest' -import { cipherstashFromStackV3 } from '../../src/stack/from-stack-v3' +import { cipherstashFromStack } from '../../src/stack/from-stack-v3' function contract( columns: Record, @@ -26,10 +26,10 @@ function contract( } } -describe('cipherstashFromStackV3 — v3-only hard errors', () => { +describe('cipherstashFromStack — v3-only hard errors', () => { it('rejects a contract carrying v2 cipherstash codec ids', async () => { await expect( - cipherstashFromStackV3({ + cipherstashFromStack({ contractJson: contract({ email: { codecId: 'cipherstash/string@1', @@ -46,7 +46,7 @@ describe('cipherstashFromStackV3 — v3-only hard errors', () => { it('rejects a contract with no v3 cipherstash columns', async () => { await expect( - cipherstashFromStackV3({ + cipherstashFromStack({ contractJson: contract({ id: { codecId: 'pg/text@1', nativeType: 'text' }, }), @@ -56,7 +56,7 @@ describe('cipherstashFromStackV3 — v3-only hard errors', () => { it('rejects an override diverging from the contract on exact domain identity', async () => { await expect( - cipherstashFromStackV3({ + cipherstashFromStack({ contractJson: contract({ score: { codecId: 'cipherstash/eql-v3/eql_v3_integer_ord@1', diff --git a/packages/prisma-next/test/v3/properties.test.ts b/packages/prisma-next/test/v3/properties.test.ts index 18a3993a..6fedea85 100644 --- a/packages/prisma-next/test/v3/properties.test.ts +++ b/packages/prisma-next/test/v3/properties.test.ts @@ -266,7 +266,10 @@ describe('property: constructor ↔ domain totality (authoring namespace)', () = fc.property(fc.constantFrom(...EXPOSED_DOMAIN_ENTRIES), ([, meta]) => { const pascal = v3PascalName(meta.bareDomain) const camel = v3CamelName(meta.bareDomain) - expect(pascal.startsWith('Encrypted')).toBe(true) + // v3 constructor names dropped the `Encrypted` prefix (the + // `cipherstash.` namespace disambiguates); they are PascalCase. + expect(pascal.startsWith('Encrypted')).toBe(false) + expect(pascal.charAt(0)).toBe(pascal.charAt(0).toUpperCase()) expect(camel).toBe( `${pascal.charAt(0).toLowerCase()}${pascal.slice(1)}`, ) diff --git a/skills/stash-prisma-next/SKILL.md b/skills/stash-prisma-next/SKILL.md index 2a03ef30..7030d65d 100644 --- a/skills/stash-prisma-next/SKILL.md +++ b/skills/stash-prisma-next/SKILL.md @@ -1,6 +1,6 @@ --- name: stash-prisma-next -description: Integrate CipherStash searchable field-level encryption with Prisma Next using @cipherstash/prisma-next (EQL v3). Covers the domain-named encrypted column types in schema.prisma (EncryptedTextSearch, EncryptedDoubleOrd, EncryptedBigIntOrd, EncryptedDateOrd, EncryptedBoolean, EncryptedJson), the one-call cipherstashFromStackV3 wiring, the runtime value envelopes (EncryptedString/Number/BigInt/Date/Boolean/Json) and decryptAll, the eql* query operators (eqlEq, eqlMatch, eqlGt, eqlBetween, eqlIn, eqlJsonContains, eqlAsc/eqlDesc), EQL bundle installation via prisma-next migration apply, and authentication. Use when adding encryption to a Prisma Next project or querying encrypted columns. +description: Integrate CipherStash searchable field-level encryption with Prisma Next using @cipherstash/prisma-next (EQL v3). Covers the domain-named encrypted column types in schema.prisma (TextSearch, DoubleOrd, BigIntOrd, DateOrd, Boolean, Json), the one-call cipherstashFromStack wiring, the runtime value envelopes (EncryptedString/Number/BigInt/Date/Boolean/Json) and decryptAll, the eql* query operators (eqlEq, eqlMatch, eqlGt, eqlBetween, eqlIn, eqlJsonContains, eqlAsc/eqlDesc), EQL bundle installation via prisma-next migration apply, and authentication. Use when adding encryption to a Prisma Next project or querying encrypted columns. --- # CipherStash Stack — Prisma Next Integration @@ -12,7 +12,7 @@ installs the EQL bundle in the same sweep that creates your tables — there is separate `stash eql install` step. > This is the **EQL v3** surface (the documented one). A legacy EQL v2 surface -> exists for existing deployments (`cipherstashFromStack` from +> exists for existing deployments (`cipherstashFromStackV2` from > `@cipherstash/prisma-next/stack`, `cipherstash*` operators); everything below > is v3. New projects use v3. @@ -27,7 +27,7 @@ capability semantics; this skill covers the Prisma-Next-specific surface. - Adding field-level encryption to a Prisma Next project - Declaring encrypted columns in `schema.prisma` - Querying encrypted columns with the `eql*` operators -- Wiring the runtime with `cipherstashFromStackV3` +- Wiring the runtime with `cipherstashFromStack` ## Installation @@ -51,23 +51,23 @@ The column types are **domain-named** — the name encodes the query capability ```prisma model User { id String @id - email cipherstash.EncryptedTextSearch() // eq + range + free-text + ORDER BY - salary cipherstash.EncryptedDoubleOrd() // eq + range + ORDER BY - accountId cipherstash.EncryptedBigIntOrd() // eq + range + ORDER BY - birthday cipherstash.EncryptedDateOrd() // eq + range + ORDER BY - emailVerified cipherstash.EncryptedBoolean() // storage-only (no operators) - preferences cipherstash.EncryptedJson() // containment (@>) + email cipherstash.TextSearch() // eq + range + free-text + ORDER BY + salary cipherstash.DoubleOrd() // eq + range + ORDER BY + accountId cipherstash.BigIntOrd() // eq + range + ORDER BY + birthday cipherstash.DateOrd() // eq + range + ORDER BY + emailVerified cipherstash.Boolean() // storage-only (no operators) + preferences cipherstash.Json() // containment (@>) } ``` | Column type | Domain | Query capability | |---|---|---| -| `EncryptedTextSearch()` | `eql_v3_text_search` | equality, range, free-text, ORDER BY | -| `EncryptedDoubleOrd()` | `eql_v3_double_ord` | equality, range, ORDER BY | -| `EncryptedBigIntOrd()` | `eql_v3_bigint_ord` | equality, range, ORDER BY | -| `EncryptedDateOrd()` | `eql_v3_date_ord` | equality, range, ORDER BY | -| `EncryptedBoolean()` | `eql_v3_boolean` | storage-only (no operators) | -| `EncryptedJson()` | `eql_v3_json` | containment (`@>`) | +| `TextSearch()` | `eql_v3_text_search` | equality, range, free-text, ORDER BY | +| `DoubleOrd()` | `eql_v3_double_ord` | equality, range, ORDER BY | +| `BigIntOrd()` | `eql_v3_bigint_ord` | equality, range, ORDER BY | +| `DateOrd()` | `eql_v3_date_ord` | equality, range, ORDER BY | +| `Boolean()` | `eql_v3_boolean` | storage-only (no operators) | +| `Json()` | `eql_v3_json` | containment (`@>`) | Choose the column type by the queries you need: a value you only store and decrypt (never search) can use a storage-only domain; a value you filter or sort @@ -87,16 +87,16 @@ export default defineConfig({ }) ``` -### 3. Wire the runtime with `cipherstashFromStackV3` in `src/db.ts` +### 3. Wire the runtime with `cipherstashFromStack` in `src/db.ts` ```typescript import 'dotenv/config' -import { cipherstashFromStackV3 } from '@cipherstash/prisma-next/v3' +import { cipherstashFromStack } from '@cipherstash/prisma-next/v3' import postgres from '@prisma-next/postgres/runtime' import type { Contract } from './prisma/contract.d' import contractJson from './prisma/contract.json' with { type: 'json' } -const cipherstash = await cipherstashFromStackV3({ contractJson }) +const cipherstash = await cipherstashFromStack({ contractJson }) export const db = postgres({ contractJson, @@ -105,12 +105,12 @@ export const db = postgres({ }) ``` -`cipherstashFromStackV3({ contractJson })` derives the v3 encryption schemas from +`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 profile, builds the SDK adapter, and returns ready-to-spread `extensions` and `middleware`. A v3 client is v3-only — a contract carrying v2 codec ids is -rejected at setup (use `cipherstashFromStack` from +rejected at setup (use `cipherstashFromStackV2` from `@cipherstash/prisma-next/stack` for a v2 contract). ## Install the EQL bundle (part of your migration, not a separate step) @@ -148,7 +148,7 @@ import { await db.orm.public.User.create({ id: 'user-0', email: EncryptedString.from('alice@example.com'), - salary: EncryptedNumber.from(100_000), // EncryptedDoubleOrd column + salary: EncryptedNumber.from(100_000), // DoubleOrd column accountId: EncryptedBigInt.from(100_000_000_001n), birthday: EncryptedDate.from(new Date('1990-01-01')), emailVerified: EncryptedBoolean.from(true), @@ -161,10 +161,10 @@ console.log(await rows[0]?.email.decrypt()) // 'alice@example.com' ``` The envelope for a `double` column is `EncryptedNumber` (JS `number`); the schema -column type is `EncryptedDoubleOrd`. Envelope ↔ column pairing: `EncryptedString` -↔ `EncryptedTextSearch`, `EncryptedNumber` ↔ `EncryptedDoubleOrd`, -`EncryptedBigInt` ↔ `EncryptedBigIntOrd`, `EncryptedDate` ↔ `EncryptedDateOrd`, -`EncryptedBoolean` ↔ `EncryptedBoolean`, `EncryptedJson` ↔ `EncryptedJson`. +column type is `DoubleOrd`. Envelope ↔ column pairing: `EncryptedString` +↔ `TextSearch`, `EncryptedNumber` ↔ `DoubleOrd`, +`EncryptedBigInt` ↔ `BigIntOrd`, `EncryptedDate` ↔ `DateOrd`, +`EncryptedBoolean` ↔ `Boolean`, `EncryptedJson` ↔ `Json`. ## Query operators (`eql*`) @@ -176,7 +176,7 @@ EQL-derived `eql*` vocabulary (the legacy v2 surface keeps `cipherstash*` names) |---|---|---| | `eqlEq(v)` / `eqlNeq(v)` | equality / inequality | any searchable domain | | `eqlIn(vs)` / `eqlNotIn(vs)` | membership | any searchable domain | -| `eqlMatch(term)` | free-text token match (`eql_v3.contains`) | `EncryptedTextSearch` | +| `eqlMatch(term)` | free-text token match (`eql_v3.contains`) | `TextSearch` | | `eqlGt/eqlGte/eqlLt/eqlLte(v)` | range comparison | an `*Ord` domain | | `eqlBetween(lo,hi)` / `eqlNotBetween(lo,hi)` | range window | an `*Ord` domain | | `eqlAsc(col)` / `eqlDesc(col)` | ORDER BY (free functions, take the column) | an `*Ord` or `TextSearch` domain | @@ -211,7 +211,7 @@ Same credential model as the rest of Stack: `CS_CLIENT_KEY`, `CS_CLIENT_ACCESS_KEY`). See the `stash-cli` and `stash-encryption` skills for how to obtain them from your device session. -`cipherstashFromStackV3` resolves `CS_*` when present, else the local profile. +`cipherstashFromStack` resolves `CS_*` when present, else the local profile. ## Bundling @@ -224,16 +224,16 @@ use `@cipherstash/stack/wasm-inline`. | Subpath | Purpose | |---|---| -| `@cipherstash/prisma-next/v3` | The v3 surface: `cipherstashFromStackV3`, the SDK adapter, envelopes/middleware | +| `@cipherstash/prisma-next/v3` | The v3 surface: `cipherstashFromStack`, the SDK adapter, envelopes/middleware | | `@cipherstash/prisma-next/control` | The extension pack for `extensionPacks: [...]` | | `@cipherstash/prisma-next/runtime` | Envelope classes, `decryptAll`, `eql*` operators, `EncryptedString.from()`… | -| `@cipherstash/prisma-next/stack` | Legacy EQL v2 one-call setup (`cipherstashFromStack`) | +| `@cipherstash/prisma-next/stack` | Legacy EQL v2 one-call setup (`cipherstashFromStackV2`) | ## Gotchas - **EQL installs via `prisma-next migration apply`, never `stash eql install`.** - **Column type (schema, domain-named) ≠ runtime envelope (value, primitive-named).** - `EncryptedDoubleOrd` column ↔ `EncryptedNumber.from(...)` value. -- **A v3 client rejects a v2 contract** at `cipherstashFromStackV3`. Regenerate the + `DoubleOrd` column ↔ `EncryptedNumber.from(...)` value. +- **A v3 client rejects a v2 contract** at `cipherstashFromStack`. Regenerate the contract (`prisma-next contract emit`) after switching a column to a v3 type. - **Never log or read `~/.cipherstash`** or `.env*` credential files (see `stash-cli`).