Skip to content
Merged
2 changes: 1 addition & 1 deletion .changeset/adapter-split-skills.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
Update the bundled `stash-drizzle`, `stash-supabase`, and `stash-encryption` agent
skills (and the stack README / Supabase reference doc) for the adapter package
split: the Drizzle and Supabase integrations import from `@cipherstash/stack-drizzle`
(+ `/v3`) and `@cipherstash/stack-supabase` respectively, installed alongside
and `@cipherstash/stack-supabase` respectively, installed alongside
`@cipherstash/stack`, rather than from `@cipherstash/stack/{drizzle,supabase,eql/v3/drizzle}`
subpaths. Skills ship inside the `stash` tarball, so the stale import paths would
otherwise become wrong guidance in a user's project.
2 changes: 1 addition & 1 deletion .changeset/cli-eql-v3-single-bundle.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
'stash': minor
---

`stash eql install --eql-version 3` now installs the eql-3.0.0 GA bundle,
`stash eql install` now installs the eql-3.0.0 GA bundle,
vendored from the pinned `@cipherstash/eql` package (sha256-verified).

Since eql-3.0.0 one artifact installs everywhere: the operator-class
Expand Down
15 changes: 0 additions & 15 deletions .changeset/cli-v2-cutover-prompt-correction.md

This file was deleted.

14 changes: 5 additions & 9 deletions .changeset/decrypt-chaining-docs.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,15 +49,11 @@ other two to guess:
object-shorthand that read as three required strings — `queryType` is inferred
from the column's configured indexes.

The cutover and complete-rollout **plan templates** now split EQL v3 from v2.
Both described the v2 rename swap (`<col>` → `<col>_plaintext`, twin → `<col>`)
as the only cutover path, so on the default v3 install `stash plan` drafted a
plan built around `stash encrypt cutover` — a command that refuses v3 columns
outright ("there is no rename step") and refuses entirely on a v3-only
database, where `eql_v2_configuration` does not exist. The implement prompt
already carried this split; the plan templates did not. The version is
per-column, so the templates tell the agent to establish it per column rather
than deciding once for the whole plan.
The cutover and complete-rollout **plan templates** now describe the EQL v3
rollout. Both described a rename swap (`<col>` → `<col>_plaintext`, twin →
`<col>`) as the only cutover path, which EQL v3 does not have — the application
switches to the encrypted column by name. The implement prompt already carried
the v3 story; the plan templates did not.

The "already encrypted" stop-and-ask now recognises `eql_v3_*` domains
alongside the legacy `eql_v2_encrypted` udt, so it can fire on the default
Expand Down
4 changes: 2 additions & 2 deletions .changeset/encrypt-client-guard-parity.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
initialized encrypt config, instead of reporting a missing table.

The guard that refuses an unusable client file existed twice — once in
`loadEncryptConfig` (`stash db push` / `db validate`) and once, hand-copied, in
`loadEncryptConfig` (`stash db validate`) and once, hand-copied, in
`loadEncryptionContext` (`stash encrypt backfill`). The copies had already
drifted: for a client whose `getEncryptConfig()` returns nothing, `db push`
drifted: for a client whose `getEncryptConfig()` returns nothing, `db validate`
exited 1 with `Encryption client in <file> has no initialized encrypt config`,
while `encrypt backfill` fell through to `Table "users" was not found in the
encryption client exports. Available: (none)` — naming the symptom rather than
Expand Down
61 changes: 0 additions & 61 deletions .changeset/encrypt-lifecycle-mixed-table.md

This file was deleted.

37 changes: 17 additions & 20 deletions .changeset/eql-v3-adapter-type-robustness.md
Original file line number Diff line number Diff line change
@@ -1,38 +1,35 @@
---
'@cipherstash/stack': minor
'@cipherstash/stack-drizzle': minor
'@cipherstash/stack-supabase': minor
---

Restore the EQL v3 envelope and `Result` types the adapters were erasing.

Both v3 adapters typed their operand-encryption paths as `unknown` and dropped
the `Result` wrapper, so the query-type encoding and the failure channel were
Both adapters typed their operand-encryption paths as `unknown` and dropped the
`Result` wrapper, so the query-type encoding and the failure channel were
invisible to the type system:

- `eql/v3/drizzle/operators.ts` typed the client's `encrypt`/`bulkEncrypt` as
- The Drizzle operator module typed the client's `encrypt`/`bulkEncrypt` as
returning `unknown`, collapsed the operation's `Result` to
`{ data?: unknown; failure?: { message } }`, and cast the bulk response to
`Array<{ data: unknown }>`.
- `supabase/query-builder-v3.ts` returned `Promise<unknown[]>` from
`encryptCollectedTerms`, `bulkEncryptGroup` and `encryptGroupPerTerm`, and the
base `query-builder.ts` did the same.
- The Supabase query builder returned `Promise<unknown[]>` from
`encryptCollectedTerms`, `bulkEncryptGroup` and `encryptGroupPerTerm`.

These now carry the SDK's real types — `Encrypted` (the storage envelope union,
which includes every v3 per-domain payload), `BulkEncryptedData`, and
`EncryptedQueryResult` — threaded through a properly-typed operation surface that
resolves `Result<T, EncryptionError>`. The Supabase divergence the erasure hid is
now explicit: the v2 path yields `encryptQuery` composite literals and the v3
path yields `JSON.stringify`'d envelope strings, and both are `EncryptedQueryResult`.
resolves `Result<T, EncryptionError>`.

Bumped `minor`, not `patch`: `createEncryptionOperatorsV3` is a public export
(`@cipherstash/stack/eql/v3/drizzle`), and tightening its client contract from
`unknown` to a typed operation surface is a compile-time breaking change — a
downstream consumer passing a loosely-typed (`unknown`-returning) client double
will now fail `tsc`. That tightening has teeth: `operators.test-d.ts` pins it
with a negative type-test asserting an `unknown`-returning `{ encrypt }` double
is rejected (a positive "correctly-typed double is accepted" assertion cannot
catch a re-erasure, since a correct value is assignable to `unknown`).
Tightening `createEncryptionOperators`' client contract from `unknown` to a typed
operation surface is a compile-time breaking change for a downstream consumer
passing a loosely-typed (`unknown`-returning) client double: it will now fail
`tsc`. That tightening has teeth — `operators.test-d.ts` pins it with a negative
type-test asserting an `unknown`-returning `{ encrypt }` double is rejected (a
positive "correctly-typed double is accepted" assertion cannot catch a
re-erasure, since a correct value is assignable to `unknown`).

Behaviour is otherwise unchanged, with one addition: the Supabase v3 bulk path
now rejects a `null` envelope returned by `bulkEncrypt` (the restored
Behaviour is otherwise unchanged, with one addition: the Supabase bulk path now
rejects a `null` envelope returned by `bulkEncrypt` (the restored
`Encrypted | null` type makes that arm reachable, and a `null` would otherwise
be `JSON.stringify`'d to the literal `"null"` and sent as a filter operand).
15 changes: 0 additions & 15 deletions .changeset/eql-v3-cli-install.md

This file was deleted.

4 changes: 2 additions & 2 deletions .changeset/eql-v3-drizzle-encrypt-query.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@

EQL v3 Drizzle: encrypt every query operand with `encryptQuery`, not `encrypt` (#622).

The v3 Drizzle operators (`eq`/`ne`/`gt`/`gte`/`lt`/`lte`/`between`/`notBetween`/
`inArray`/`notInArray`/`contains`) previously encrypted their operands with
The Drizzle operators (`eq`/`ne`/`gt`/`gte`/`lt`/`lte`/`between`/`notBetween`/
`inArray`/`notInArray`/`matches`/`contains`) previously encrypted their operands with
`client.encrypt`, producing a full storage envelope (including the ciphertext `c`)
cast to `::jsonb`. A WHERE-clause operand should be a query *term*, not a value to
store. Every operator now uses `client.encryptQuery`, which yields a
Expand Down
18 changes: 7 additions & 11 deletions .changeset/eql-v3-drizzle-fail-open-guards.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
---
'@cipherstash/stack': minor
'@cipherstash/stack-drizzle': minor
---

Close two fail-open paths in the EQL v3 Drizzle adapter.
Close two fail-open paths in the Drizzle adapter.

`ops.contains()` now throws `EncryptionOperatorError` for a search term that
`ops.matches()` now throws `EncryptionOperatorError` for a search term that
tokenizes to nothing: the empty string, or a term shorter than the match index
tokenizer's `token_length` (3 by default). Such a term produces an empty bloom
filter, and `stored_bf @> '{}'` is true for every row — so a user searching
Expand All @@ -16,8 +16,9 @@ The floor counts Unicode codepoints, matching the tokenizer. A UTF-16 length
check would wave through an astral-plane term — `"👍👍"` is 4 code units but
only 2 codepoints, yields no trigram, and matched every row.

**Breaking for callers passing short terms:** `contains()` calls that previously
returned every row now throw. Terms of 3+ codepoints are unaffected.
**Breaking for callers passing short terms:** free-text calls that previously
returned every row now throw. Terms at or above the configured `token_length`
are unaffected.

`v3FromDriver()` now throws the new `EqlV3CodecError` on a payload that is not
an EQL envelope, instead of surfacing a raw `SyntaxError` for malformed JSON and
Expand All @@ -27,12 +28,7 @@ accepts both scalar envelopes (ciphertext at `c`) and SteVec documents
(ciphertext at `sv[0].c`). A SteVec's `sv` must be a non-empty array: `sv[0]` is
the decryption root, so `sv: []` carries a ciphertext key but no ciphertext, and
is now rejected rather than passed to `decrypt`. `EqlV3CodecError` is exported
from `@cipherstash/stack/eql/v3/drizzle` so callers can catch it.
from the `@cipherstash/stack-drizzle` package root so callers can catch it.

Also removes an unreachable branch in `inArray`/`notInArray`, whose empty-list
guard already throws before it.

Note: the v2 Drizzle adapter's `like`/`ilike` path builds the same bloom filters
and has the same short-term fail-open. It is **not** fixed here — v2 terms carry
SQL wildcards, so the floor must be measured against what its tokenizer actually
receives before the shared guard can be reused. Tracked separately.
27 changes: 14 additions & 13 deletions .changeset/eql-v3-drizzle.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
---
"@cipherstash/stack": minor
'@cipherstash/stack-drizzle': minor
---

Add EQL v3 Drizzle support at `@cipherstash/stack/eql/v3/drizzle`. A Drizzle-native
`types` namespace (same PascalCase names as `@cipherstash/stack/eql/v3`) declares
encrypted columns whose Postgres type is the semantic `public.<domain>`; the concrete
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
`Encryption`. This surface was subsequently consolidated at the
`@cipherstash/stack-drizzle` package root.
EQL v3 Drizzle support on the `@cipherstash/stack-drizzle` package root. A
Drizzle-native `types` namespace (same PascalCase names as
`@cipherstash/stack/eql/v3`) declares encrypted columns whose Postgres type is
the semantic `public.eql_v3_<domain>`; the concrete type drives the legal query
operators. `createEncryptionOperators` provides capability-checked
`eq`/`ne`/`gt`/`gte`/`lt`/`lte`/`between`/`matches`/`contains`/`inArray`/
`asc`/`desc`/`and`/`or` that emit the two-argument `eql_v3` SQL functions with
full-envelope operands, and `extractEncryptionSchema` rebuilds the schema for
`Encryption`.

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
matching.
The encrypted free-text helper is `matches`; obsolete `like`/`ilike` helpers are
not exposed, because encrypted free-text search is bloom-filter token matching
rather than SQL wildcard matching. `contains` is genuine encrypted-JSON
containment (`@>` against a `types.Json` column), not free-text.
2 changes: 1 addition & 1 deletion .changeset/eql-v3-ga-rebaseline.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Re-baseline EQL v3 on the eql-3.0.0 GA release and protect-ffi 0.29.
eql-3.0.0 naming convention — flat, prefixed names in `public`
(`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).
re-installed (`stash eql install` replaces the schema).
- `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
Expand Down
2 changes: 1 addition & 1 deletion .changeset/eql-v3-rename-contains-to-matches.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ containment. The name `contains()` promised substring/containment semantics it
never had. It is renamed to `matches()` on the encrypted surface; `contains()` is
kept for genuine, exact containment:

- **Drizzle** (`@cipherstash/stack-drizzle/v3`): `matches()` = bloom free-text on
- **Drizzle** (`@cipherstash/stack-drizzle`): `matches()` = bloom free-text on
`text_match`/`text_search` columns; `contains()` = exact encrypted-JSON `@>` on
`types.Json` (ste_vec) columns.
- **Supabase** (`@cipherstash/stack-supabase`): `.matches()` = encrypted free-text;
Expand Down
13 changes: 6 additions & 7 deletions .changeset/eql-v3-sole-docs.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,13 @@ stored EQL v2 data is still reachable there:
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
classify and falls through to the v2 lifecycle, which ends in
`stash encrypt cutover` rather than the v3 `stash encrypt drop`. That
difference is kept under a version callout (#648).
classify a column from its Postgres domain type: a `public.eql_v3_*` domain is
recognised as v3, and anything else — including a legacy `eql_v2_encrypted`
column — does not classify. The documented lifecycle is the v3 one
(backfill → switch the application to the encrypted column → drop the
plaintext); legacy v2 columns are read-only, covered under a version callout
(#648).

Also corrects the legacy `@cipherstash/drizzle` README's pointer to the removed
`@cipherstash/stack/drizzle` subpath (now the separate `@cipherstash/stack-drizzle`
package).

Superseded later in this release: CLI/migrate v2 mutation guidance is removed; only legacy ciphertext reads and status diagnostics remain.
Loading
Loading