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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 6 additions & 7 deletions .changeset/adapter-package-split.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,17 @@ Split the Drizzle and Supabase integrations into their own packages.
The adapters now ship as first-party packages that depend on `@cipherstash/stack`,
following the `@cipherstash/prisma-next` precedent:

- **`@cipherstash/stack-drizzle`** — Drizzle ORM integration. EQL v2 on the package
root (`@cipherstash/stack-drizzle`: `encryptedType`, `extractEncryptionSchema`,
`createEncryptionOperators`) and EQL v3 on `@cipherstash/stack-drizzle/v3`
(`types` factories, `createEncryptionOperatorsV3`, `extractEncryptionSchemaV3`, …).
- **`@cipherstash/stack-supabase`** — Supabase integration: `encryptedSupabase` (v2)
and `encryptedSupabaseV3` (v3, connect-time introspection).
- **`@cipherstash/stack-drizzle`** — EQL v3 Drizzle integration on the package
root (`types` factories, `createEncryptionOperators`,
`extractEncryptionSchema`, …).
- **`@cipherstash/stack-supabase`** — EQL v3 Supabase integration through the
connect-time-introspecting `encryptedSupabase` factory.

**Breaking (`@cipherstash/stack`):** the `./drizzle`, `./supabase`, and
`./eql/v3/drizzle` subpath exports are removed. Migrate imports:

- `@cipherstash/stack/drizzle` → `@cipherstash/stack-drizzle`
- `@cipherstash/stack/eql/v3/drizzle` → `@cipherstash/stack-drizzle/v3`
- `@cipherstash/stack/eql/v3/drizzle` → `@cipherstash/stack-drizzle`
- `@cipherstash/stack/supabase` → `@cipherstash/stack-supabase`

Add the relevant package to your dependencies alongside `@cipherstash/stack`. A new
Expand Down
25 changes: 25 additions & 0 deletions .changeset/decrypt-lock-context-binds-once.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
'@cipherstash/stack': patch
---

`decryptModel` / `bulkDecryptModels` now drop `.withLockContext()` from the
returned operation once a lock context is bound, so binding twice is a compile
error instead of a runtime throw.

Passing a lock context positionally and then chaining it —
`client.decryptModel(row, users, lockContext).withLockContext(lockContext)` —
type-checked, then threw `this decrypt operation is already bound to a lock
context`. The type promised a method the runtime rejected.

The operation interface is split to match what the encrypt path already does,
where `EncryptModelOperationWithLockContext` simply lacks the method: the
new `LockBoundDecryptModelOperation` carries `.audit()` only, and
`AuditableDecryptModelOperation.withLockContext()` returns it. The runtime throw
stays as the backstop for plain-JavaScript callers.

An OPTIONAL lock context still type-checks. `decryptModel(row, users,
session?.lockContext)` — where the value is `LockContextInput | undefined` — is
the ordinary shape for code that decrypts identity-bound rows only for
signed-in users, and it compiled against the single optional parameter this
replaces. The positional overloads accept `LockContextInput | undefined` so it
keeps compiling; `undefined` binds nothing.
6 changes: 3 additions & 3 deletions .changeset/drizzle-kit-eql-v3-ddl.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ hand-repaired.

The v3 column now emits the **unqualified** domain (`eql_v3_text_search`), which
drizzle-kit renders as the valid `"eql_v3_text_search"` and which resolves via the
search path (the domains live in `public`). This matches how the v2
`encryptedType` surface already declares its type, and how drizzle-kit reads the
type back during a `push` introspection diff, so the two sides no longer disagree.
search path (the domains live in `public`). This also matches how drizzle-kit
reads the type back during a `push` introspection diff, so the two sides no
longer disagree.
Builder recovery still yields the canonical `public.eql_v3_*` identity, so
operators and schema extraction are unchanged.

Expand Down
13 changes: 5 additions & 8 deletions .changeset/dynamodb-eql-v3.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,9 @@ Pass a table built with `encryptedTable` + the `types.*` domains from
`decryptModel`, or `bulkDecryptModels`. Build the typed client with
`Encryption({ schemas: [table] })`.

EQL v2 tables continue to be **readable** — `decryptModel` /
`bulkDecryptModels` still accept one, so existing items stay accessible. Writing
through a v2 table is a separate matter: `encryptModel` / `bulkEncryptModels`
narrowed to EQL v3 tables in this same release, so a caller that still encrypts
through a v2 table does need to change. The table decides which wire format is
used, so a DynamoDB table populated under one version must keep being read with
that version.
Existing EQL v2 items continue to be **readable**: pass the corresponding EQL v3
table plus `{ storedEqlVersion: 2 }` to `decryptModel` /
`bulkDecryptModels`. Writes accept EQL v3 tables only.

This fixes a latent bug that made v3 unusable: the write path detected an
encrypted value by its `k: 'ct'` tag, but EQL v3 scalars carry no `k`
Expand Down Expand Up @@ -73,4 +69,5 @@ type, where a declared column `email` becomes `email__source` (plus
`email`. `decryptModel` / `bulkDecryptModels` invert it via `DecryptedAttributes`.
`AnyEncryptedTable`, `DynamoDBEncryptionClient` and `AuditConfig` are now
exported from `@cipherstash/stack/dynamodb` so these signatures can be named.
The EQL v2 **decrypt** overloads are unchanged; the v2 encrypt overloads are removed in this release.
Legacy reads use the explicit storage-version option rather than an EQL v2 table
overload; the v2 encrypt overloads are removed in this release.
13 changes: 6 additions & 7 deletions .changeset/dynamodb-skill-wasm-caveat.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,12 @@
'stash': patch
---

The `stash-dynamodb` and `stash-encryption` skills documented EQL v2 decrypt as
unconditionally supported, without noting that `@cipherstash/stack/wasm-inline`
is EQL v3 only. Since that is the documented entry for Deno, Bun, Cloudflare
Workers and Supabase Edge Functions — runtimes commonly paired with DynamoDB — a
reader following the skill hit a runtime refusal with no forewarning. Both skills
now state that legacy v2 items are readable on the native `@cipherstash/stack`
entry only.
The `stash-dynamodb` and `stash-encryption` skills now state which entries serve
a legacy EQL v2 DynamoDB read: both of them. Schema authoring is EQL v3-only
everywhere, but the read is not — it reconstructs the v2 envelope around the
current v3 table, and `decrypt` accepts either wire generation. Deno, Bun,
Cloudflare Workers and Supabase Edge Functions can therefore read pre-migration
items through `@cipherstash/stack/wasm-inline`.

The `stash-dynamodb` API reference also claimed audit metadata forwards to
ZeroKMS "regardless of client shape". It does not: the wasm-inline client's
Expand Down
25 changes: 25 additions & 0 deletions .changeset/dynamodb-v2-grouped-field-reads.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
'@cipherstash/stack': patch
---

Fix reading a nested EQL v2 DynamoDB attribute that was stored under a grouped
field.

A v2 grouped column registered its build key on the BARE LEAF, so a field inside
a group was written as `<group>.<leaf>__source` while the schema knew it only as
`<leaf>`. The v3 rewrite made attribute matching exact-dotted-path only, for both
generations, which orphaned every such attribute: it read back as raw base64
inside a `{ data }` success, with only a debug-level log — invisible at the
default log level. Silent wrong data on the one path that exists for backward
compatibility.

The bare-leaf fallback is restored for `{ storedEqlVersion: 2 }` reads only.
It stays off for v3, where full dotted paths are registered precisely so a
nested leaf cannot collide with a same-named top-level column — matching by
bare leaf there rewrote a plaintext sibling as an envelope and handed it to the
FFI as a decrypt target. Writes are EQL v3-only and stay strict.

If you carried a v2 grouped column forward as a top-level v3 column, you can
also declare it by its dotted path with the original DB name —
`'details.amount': types.TextEq('amount')` — which reproduces the v2 identifier
exactly.
6 changes: 4 additions & 2 deletions .changeset/dynamodb-v2-read-table-forwarding.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@ but decrypt keeps accepting a v2 table so previously stored items stay readable.
In practice the failure hit exactly the customers the compatibility promise was
written for: upgraded to a v3 schema, with v2 items still in the table.

The table is now forwarded only when it is an EQL v3 table. A v2 table takes the
table-less form, and the client derives the table from the payloads themselves.
The current v3 table is now forwarded on every read, legacy storage included:
the legacy path reconstructs the v2 envelope around it, and the reconstructor
map is keyed by the current schema either way. (Schema authoring is EQL v3-only,
so there is no longer any such thing as a v2 table object to pass.)

Both paths are covered by a credential-free test asserting what the adapter
forwards, so a regression no longer depends on a live-credential integration run
Expand Down
26 changes: 10 additions & 16 deletions .changeset/dynamodb-wasm-v2-read.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,18 @@
'@cipherstash/stack': patch
---

`encryptedDynamoDB` now refuses a `@cipherstash/stack/wasm-inline` client paired
with a legacy EQL v2 table, instead of failing at the first read with a
misleading error.
`encryptedDynamoDB` now serves legacy `{ storedEqlVersion: 2 }` reads on the
`@cipherstash/stack/wasm-inline` entry, not just the native one.

The adapter's v2 read path deliberately calls `decryptModel(item)` with **no**
table — a v2 table means nothing to a v3 client's reconstructor map, and the
native clients derive the table from the payloads anyway. `WasmEncryptionClient`
cannot do that: its decrypt requires the table and resolves date fields from a
per-table map, so the omitted argument surfaced as
`TypeError: Cannot read properties of undefined (reading 'tableName')` thrown
from deep inside the client — on the documented entry for Deno, Cloudflare
Workers and Supabase Edge Functions, which satisfies the adapter's client type
structurally and so was accepted with no cast.
The legacy read reconstructs the EQL v2 envelope around the **current v3 table**
and forwards that table exactly as a v3 read does, so both entries can serve it:
protect-ffi's `decrypt` accepts either wire generation regardless of the
client's `eqlVersion`, and the reconstructor map is keyed by the current schema
either way. Deno, Cloudflare Workers and Supabase Edge Functions can therefore
read rows written before the v3 migration; previously the pairing was refused
outright and those runtimes had no way to read that data at all.

The pairing is now rejected at the call site, with a message naming both the
combination and the fix. The message is operation-neutral: the guard runs on all
four operations, so a plain-JS caller reaching the write path with a v2 table
gets a message that does not claim a read it never attempted.
Writes remain EQL v3-only on both entries.

`encryptModel` / `bulkEncryptModels` now also tolerate a client whose encrypt
returns a plain promise. They chained `.audit()` onto the result
Expand Down
16 changes: 11 additions & 5 deletions .changeset/encryption-schema-arrays.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,16 @@ an array literal still gets full per-column typing — passing the wrong plainte
for a column's domain, or a table the client was not built with, is still
rejected.

`EncryptionClientFor<S>` is widened to match, so it names the typed client for a
loose `readonly AnyV3Table[]` as well as for a tuple. Code that is generic over
its schemas — an adapter that builds a table per request, say — can now write
`EncryptionClientFor<readonly AnyV3Table[]>` and keep the typed surface.
`EncryptionClient<S>` accepts the same schema parameter, so it names the client
for a loose `readonly AnyV3Table[]` as well as for a tuple. Code that is generic
over its schemas — an adapter that builds a table per request, say — can write
`EncryptionClient<readonly AnyV3Table[]>`.

That keeps the `table` and `column` arguments checked, but not the model input:
with the schema parameter loose there is no per-column plaintext to resolve, so
`encryptModel` / `bulkEncryptModels` still reject an untyped
`Record<string, unknown>` model and an adapter holding untyped rows needs a cast
at that one boundary. Full model typing requires a concrete schema tuple.

If you narrowed a schema array to `readonly [AnyV3Table, ...AnyV3Table[]]` to
satisfy the old signature, that narrowing is no longer needed.
Expand All @@ -48,5 +54,5 @@ async function makeClient<S extends readonly [AnyV3Table, ...AnyV3Table[]]>(
Note the non-empty tuple constraint. A wrapper generic over a loose
`readonly AnyV3Table[]` is still rejected — that type admits `readonly []`, so
the wrapper cannot promise what `Encryption` requires. Constrain it as above, or
take `EncryptionClientFor<readonly AnyV3Table[]>` as a parameter instead of
take `EncryptionClient<readonly AnyV3Table[]>` as a parameter instead of
building the client inside the generic function.
7 changes: 3 additions & 4 deletions .changeset/eql-v3-drizzle-encrypt-query.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ operator overloads. This unifies the scalar/text operators with the JSON
containment path (already on `encryptQuery`) and removes the previously-optional
`encryptQuery` guard: it is now a required capability of the operand client.

`@cipherstash/stack` gains a batch `encryptQuery(terms)` overload on
`TypedEncryptionClient` (the type `EncryptionV3` returns), mirroring the nominal
`EncryptionClient`. This is additive — it lets `inArray`/`notInArray` encrypt a
whole list of query terms in one crossing.
`@cipherstash/stack` gains a batch `encryptQuery(terms)` overload on the generic
`EncryptionClient<S>` returned by `Encryption`. This is additive — it lets
`inArray`/`notInArray` encrypt a whole list of query terms in one crossing.
6 changes: 3 additions & 3 deletions .changeset/eql-v3-drizzle.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ encrypted columns whose Postgres type is the semantic `public.<domain>`; the con
type drives the legal query operators. `createEncryptionOperatorsV3` provides
capability-checked `eq`/`ne`/`gt`/`gte`/`lt`/`lte`/`between`/`contains`/`inArray`/
`asc`/`desc`/`and`/`or` that emit the latest two-argument `eql_v3` SQL functions with
full-envelope operands, and
`extractEncryptionSchemaV3` rebuilds the schema for `EncryptionV3`. The existing v2
`@cipherstash/stack/drizzle` integration is unchanged.
full-envelope operands, and `extractEncryptionSchemaV3` rebuilds the schema for
`Encryption`. This surface was subsequently consolidated at the
`@cipherstash/stack-drizzle` package root.

The v3 text-search helper is `contains`; obsolete `like`/`ilike` helpers are not
exposed because v3 free-text search is token containment rather than SQL wildcard
Expand Down
11 changes: 5 additions & 6 deletions .changeset/eql-v3-ga-rebaseline.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,14 @@ Re-baseline EQL v3 on the eql-3.0.0 GA release and protect-ffi 0.29.
(`public.eql_v3_text_search`, `public.eql_v3_integer_ord`, …) instead of the
alpha-era bare names. Databases installed from an alpha bundle must be
re-installed (`stash eql install --eql-version 3` replaces the schema).
- `encryptQuery` under `eqlVersion: 3` now returns EQL v3 query operands
- `encryptQuery` on the EQL v3 client now returns EQL v3 query operands
(protect-ffi 0.29): term-only scalar operands for the `eql_v3.query_<name>`
domains, the `eql_v3.query_jsonb` containment needle, and bare selector
hashes for JSON path queries — v3 scalar and selector queries no longer
throw `EQL_V3_QUERY_UNSUPPORTED` (the code is gone).
- v2 `searchableJson()` columns now pin the SteVec encoding to `standard`
explicitly. protect-ffi 0.29 flipped the library default to `compat`
(EQL v3's encoding); without the pin, v2 JSON containment queries would
silently match nothing and newly written rows would not be comparable with
existing ones.
- Legacy EQL v2 JSON compatibility fixtures pin the SteVec encoding to
`standard` explicitly. protect-ffi 0.29 flipped the library default to
`compat` (EQL v3's encoding); without the pin, v2 JSON containment fixtures
would silently mismatch existing data.
- The EQL v3 test/install SQL is sourced from the pinned `@cipherstash/eql`
package (3.0.0) instead of a hand-vendored fixture.
5 changes: 2 additions & 3 deletions .changeset/eql-v3-prisma-next.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,8 @@
- `EncryptedJson` — searchable encrypted JSONB (`public.eql_v3_json`, `ste_vec`), queried with `eqlJsonContains` (`@>` containment). Selector querying (comparing the value at a JSONPath) is tracked in #677.
- **Impossible capability combinations have no constructor** (e.g. text equality + free-text without order/range) — they are unrepresentable, not runtime errors.
- **BigInt is a first-class v3 family** (`EncryptedBigInt` / `EncryptedBigIntEq` / `EncryptedBigIntOrd`, JS `bigint` plaintext, backed by `public.eql_v3_bigint*`).
- Use the `*V2` constructors (`EncryptedStringV2`, `EncryptedDoubleV2`, `EncryptedBigIntV2`, `EncryptedDateV2`, `EncryptedBooleanV2`, `EncryptedJsonV2`) to keep EQL v2 columns. A client is v2 or v3 — the two runtime descriptors are never co-registered.
- New `@cipherstash/prisma-next/v3` entry point: `cipherstashFromStackV3({ contractJson })` builds the v3 runtime descriptor, bulk-encrypt middleware, and a stack `EncryptionV3` client from the emitted contract.
- Query operators use an **EQL-derived vocabulary** (`eqlEq`, `eqlNeq`, `eqlIn`, `eqlNotIn`, `eqlGt`, `eqlGte`, `eqlLt`, `eqlLte`, `eqlBetween`, `eqlNotBetween`, `eqlJsonContains`; ordering via `eqlAsc` / `eqlDesc`), lowering to the same-named `eql_v3.*` functions with operands cast to the domain's query type (`$n::eql_v3.query_<domain>`); ordering uses `eql_v3.ord_term` / `eql_v3.ord_term_ore` by the column's ordering flavour. The domains are `public.eql_v3_*`; the operator functions live in the `eql_v3` schema. (The v2 surface keeps its `cipherstash*` names.)
- New `@cipherstash/prisma-next/v3` entry point: `cipherstashFromStackV3({ contractJson })` builds the v3 runtime descriptor, bulk-encrypt middleware, and a stack `Encryption` client from the emitted contract.
- Query operators use an **EQL-derived vocabulary** (`eqlEq`, `eqlNeq`, `eqlIn`, `eqlNotIn`, `eqlGt`, `eqlGte`, `eqlLt`, `eqlLte`, `eqlBetween`, `eqlNotBetween`, `eqlJsonContains`; ordering via `eqlAsc` / `eqlDesc`), lowering to the same-named `eql_v3.*` functions with operands cast to the domain's query type (`$n::eql_v3.query_<domain>`); ordering uses `eql_v3.ord_term` / `eql_v3.ord_term_ore` by the column's ordering flavour. The domains are `public.eql_v3_*`; the operator functions live in the `eql_v3` schema.
- Free-text search is **`eqlMatch`** — fuzzy bloom token matching (`eql_v3.contains`), deliberately NOT named after SQL `ILIKE`: matching is case-insensitive, order/multiplicity-insensitive, and one-sided (may false-positive). Two guards run before encryption: SQL wildcards are normalised (leading/trailing `%` stripped; interior `%` or any `_` rejected), and needles the column's match index cannot answer (empty / below the tokenizer length) are rejected via the shared `matchNeedleError` guard. There is **no negated match operator** — negating a may-false-positive bloom test would silently drop matching rows.
- A new baseline migration `20260601T0100_install_eql_v3_bundle` (invariant `cipherstash:install-eql-v3-bundle-v1`) installs the `public.eql_v3_*` domains and `eql_v3.*` functions from the pinned `@cipherstash/eql` release. Regenerate contracts and run migrations after changing constructors.
- **The v3 ORM surface is fully wired end-to-end** (proven by converting `examples/prisma`):
Expand Down
10 changes: 5 additions & 5 deletions .changeset/eql-v3-sole-docs.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@ Docs: EQL v3 is now the sole documented approach. The `stash-encryption`,
`stash-drizzle`, and `stash-supabase` skills and the `@cipherstash/stack`
README teach only the v3 typed surface (`Encryption`, the `types.*` concrete
domains, the `@cipherstash/stack-drizzle` package root, `encryptedSupabase`);
EQL v2 shrinks to one short Legacy section per document. Two places keep more
than a Legacy section, because EQL v2 is still reachable there:
EQL v2 shrinks to read-compatibility notes. Two places keep more detail because
stored EQL v2 data is still reachable there:

- **DynamoDB reads.** `encryptedDynamoDB` writes EQL v3 only, but `decryptModel`
/ `bulkDecryptModels` still accept an EQL v2 table so previously stored v2
items stay readable, so the `stash-dynamodb` skill keeps the v2 schema shape
documented for that read path (#657).
/ `bulkDecryptModels` can read previously stored v2 items when passed the
corresponding v3 table and `{ storedEqlVersion: 2 }`, so the
`stash-dynamodb` skill documents that explicit compatibility path (#657).
- **The encrypt rollout lifecycle.** `stash encrypt *` and `@cipherstash/migrate`
detect a column's generation from its Postgres domain type, with EQL v3 as the
recognised and default generation; a legacy `eql_v2_encrypted` column does not
Expand Down
Loading
Loading