Skip to content

Commit 7752cae

Browse files
authored
Merge pull request #535 from cipherstash/feat/eql-v3-text-search-schema
feat(stack,cli): EQL v3 typed schema, typed client, Supabase/Drizzle integrations on eql-3.0.0 + protect-ffi 0.29
2 parents 992c270 + a4b9746 commit 7752cae

276 files changed

Lines changed: 51562 additions & 96160 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
---
2+
'@cipherstash/stack': minor
3+
'@cipherstash/stack-drizzle': minor
4+
'@cipherstash/stack-supabase': minor
5+
---
6+
7+
Split the Drizzle and Supabase integrations into their own packages.
8+
9+
The adapters now ship as first-party packages that depend on `@cipherstash/stack`,
10+
following the `@cipherstash/prisma-next` precedent:
11+
12+
- **`@cipherstash/stack-drizzle`** — Drizzle ORM integration. EQL v2 on the package
13+
root (`@cipherstash/stack-drizzle`: `encryptedType`, `extractEncryptionSchema`,
14+
`createEncryptionOperators`) and EQL v3 on `@cipherstash/stack-drizzle/v3`
15+
(`types` factories, `createEncryptionOperatorsV3`, `extractEncryptionSchemaV3`, …).
16+
- **`@cipherstash/stack-supabase`** — Supabase integration: `encryptedSupabase` (v2)
17+
and `encryptedSupabaseV3` (v3, connect-time introspection).
18+
19+
**Breaking (`@cipherstash/stack`):** the `./drizzle`, `./supabase`, and
20+
`./eql/v3/drizzle` subpath exports are removed. Migrate imports:
21+
22+
- `@cipherstash/stack/drizzle``@cipherstash/stack-drizzle`
23+
- `@cipherstash/stack/eql/v3/drizzle``@cipherstash/stack-drizzle/v3`
24+
- `@cipherstash/stack/supabase``@cipherstash/stack-supabase`
25+
26+
Add the relevant package to your dependencies alongside `@cipherstash/stack`. A new
27+
`@cipherstash/stack/adapter-kit` subpath exposes the narrow core internals the
28+
first-party adapters consume; it is the core↔adapter seam, not general-purpose API.

.changeset/adapter-split-skills.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
'stash': patch
3+
---
4+
5+
Update the bundled `stash-drizzle`, `stash-supabase`, and `stash-encryption` agent
6+
skills (and the stack README / Supabase reference doc) for the adapter package
7+
split: the Drizzle and Supabase integrations import from `@cipherstash/stack-drizzle`
8+
(+ `/v3`) and `@cipherstash/stack-supabase` respectively, installed alongside
9+
`@cipherstash/stack`, rather than from `@cipherstash/stack/{drizzle,supabase,eql/v3/drizzle}`
10+
subpaths. Skills ship inside the `stash` tarball, so the stale import paths would
11+
otherwise become wrong guidance in a user's project.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
'stash': minor
3+
---
4+
5+
`stash eql install --eql-version 3` now installs the eql-3.0.0 GA bundle,
6+
vendored from the pinned `@cipherstash/eql` package (sha256-verified).
7+
8+
Since eql-3.0.0 one artifact installs everywhere: the operator-class
9+
statements self-skip when the role lacks superuser (managed Postgres,
10+
Supabase) and the bundle disables the ORE-backed encrypted domains it cannot
11+
support. The separate v3 Supabase bundle variant is gone — `--supabase` and
12+
`--exclude-operator-family` no longer select a different v3 file (the role
13+
GRANTs for `eql_v3` / `eql_v3_internal` still apply with `--supabase`).
14+
15+
The bundled skills are also refreshed for the eql-3.0.0 naming convention
16+
(`public.eql_v3_<name>` column domains) and the EQL v3 typed-schema surface.
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
---
2+
'@cipherstash/stack': minor
3+
---
4+
5+
Restore the EQL v3 envelope and `Result` types the adapters were erasing.
6+
7+
Both v3 adapters typed their operand-encryption paths as `unknown` and dropped
8+
the `Result` wrapper, so the query-type encoding and the failure channel were
9+
invisible to the type system:
10+
11+
- `eql/v3/drizzle/operators.ts` typed the client's `encrypt`/`bulkEncrypt` as
12+
returning `unknown`, collapsed the operation's `Result` to
13+
`{ data?: unknown; failure?: { message } }`, and cast the bulk response to
14+
`Array<{ data: unknown }>`.
15+
- `supabase/query-builder-v3.ts` returned `Promise<unknown[]>` from
16+
`encryptCollectedTerms`, `bulkEncryptGroup` and `encryptGroupPerTerm`, and the
17+
base `query-builder.ts` did the same.
18+
19+
These now carry the SDK's real types — `Encrypted` (the storage envelope union,
20+
which includes every v3 per-domain payload), `BulkEncryptedData`, and
21+
`EncryptedQueryResult` — threaded through a properly-typed operation surface that
22+
resolves `Result<T, EncryptionError>`. The Supabase divergence the erasure hid is
23+
now explicit: the v2 path yields `encryptQuery` composite literals and the v3
24+
path yields `JSON.stringify`'d envelope strings, and both are `EncryptedQueryResult`.
25+
26+
Bumped `minor`, not `patch`: `createEncryptionOperatorsV3` is a public export
27+
(`@cipherstash/stack/eql/v3/drizzle`), and tightening its client contract from
28+
`unknown` to a typed operation surface is a compile-time breaking change — a
29+
downstream consumer passing a loosely-typed (`unknown`-returning) client double
30+
will now fail `tsc`. That tightening has teeth: `operators.test-d.ts` pins it
31+
with a negative type-test asserting an `unknown`-returning `{ encrypt }` double
32+
is rejected (a positive "correctly-typed double is accepted" assertion cannot
33+
catch a re-erasure, since a correct value is assignable to `unknown`).
34+
35+
Behaviour is otherwise unchanged, with one addition: the Supabase v3 bulk path
36+
now rejects a `null` envelope returned by `bulkEncrypt` (the restored
37+
`Encrypted | null` type makes that arm reachable, and a `null` would otherwise
38+
be `JSON.stringify`'d to the literal `"null"` and sent as a filter operand).
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
'@cipherstash/stack': minor
3+
---
4+
5+
Add the EQL v3 bigint domain family to the public DSL: `types.Bigint`,
6+
`types.BigintEq`, `types.BigintOrdOre`, and `types.BigintOrd`, backed by the
7+
`public.bigint*` concrete domains. Plaintext is a JS `bigint`, round-tripped
8+
losslessly across the protect-ffi 0.28 boundary (i64 bounds enforced at the
9+
FFI — out-of-range values surface as encryption errors). Index emission follows
10+
the numeric rule: `bigint_eq` → unique (hm); `bigint_ord`/`bigint_ord_ore`
11+
ore (equality answered via ob).
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
'@cipherstash/stack': patch
3+
---
4+
5+
Source the EQL v3 install bundle from `@cipherstash/eql@3.0.0-alpha.3` instead of a hand-vendored 43k-line SQL fixture committed to the test tree. The package publishes its SQL and its TypeScript wire types from the same `eql-bindings` commit, so the bundle is now pinned to a released EQL version rather than tracked by convention.
6+
7+
Test-and-tooling only — `@cipherstash/eql` is a `devDependency` and no public API changes.
8+
9+
The staleness check in the v3 install helper now compares `eql_v3.version()` against the pinned release instead of probing for a hand-picked sentinel domain. The previous sentinel (`public.timestamp`) exists in both the old and new bundles, so it would have reported a stale install as current and left the suite silently running the wrong SQL.

.changeset/eql-v3-cli-install.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
"stash": minor
3+
---
4+
5+
Add an EQL v3 install path to `stash eql install` via a new `--eql-version <2|3>`
6+
flag (default `2`). v3 installs the native concrete-domain schema (`public.*`
7+
type domains, `eql_v3` operators, `eql_v3_internal` constructors) from bundles
8+
vendored into `packages/cli/src/sql` by `scripts/build-eql-v3-sql.mjs` (full
9+
bundle + a Supabase variant with the two superuser-only operator-class chunks
10+
stripped). v3 currently supports the direct install path only —
11+
`--drizzle`/`--migration`/`--migrations-dir`/`--latest` are rejected — and the
12+
installer keys `isInstalled`/version checks and Supabase grants to the `eql_v3`
13+
schema.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
'@cipherstash/stack-drizzle': minor
3+
'@cipherstash/stack': minor
4+
---
5+
6+
EQL v3 Drizzle: encrypt every query operand with `encryptQuery`, not `encrypt` (#622).
7+
8+
The v3 Drizzle operators (`eq`/`ne`/`gt`/`gte`/`lt`/`lte`/`between`/`notBetween`/
9+
`inArray`/`notInArray`/`contains`) previously encrypted their operands with
10+
`client.encrypt`, producing a full storage envelope (including the ciphertext `c`)
11+
cast to `::jsonb`. A WHERE-clause operand should be a query *term*, not a value to
12+
store. Every operator now uses `client.encryptQuery`, which yields a
13+
ciphertext-free query term cast to the column's `eql_v3.query_<domain>` type — so
14+
predicates carry no ciphertext and reach the bundle's `(domain, query_<domain>)`
15+
operator overloads. This unifies the scalar/text operators with the JSON
16+
containment path (already on `encryptQuery`) and removes the previously-optional
17+
`encryptQuery` guard: it is now a required capability of the operand client.
18+
19+
`@cipherstash/stack` gains a batch `encryptQuery(terms)` overload on
20+
`TypedEncryptionClient` (the type `EncryptionV3` returns), mirroring the nominal
21+
`EncryptionClient`. This is additive — it lets `inArray`/`notInArray` encrypt a
22+
whole list of query terms in one crossing.
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
---
2+
'@cipherstash/stack': minor
3+
---
4+
5+
Close two fail-open paths in the EQL v3 Drizzle adapter.
6+
7+
`ops.contains()` now throws `EncryptionOperatorError` for a search term that
8+
tokenizes to nothing: the empty string, or a term shorter than the match index
9+
tokenizer's `token_length` (3 by default). Such a term produces an empty bloom
10+
filter, and `stored_bf @> '{}'` is true for every row — so a user searching
11+
`"ad"` silently received the entire table. Measured live, the terms `"ad"`,
12+
`"a"` and `"x"` each returned every seeded row, including one in which `"x"`
13+
did not appear.
14+
15+
The floor counts Unicode codepoints, matching the tokenizer. A UTF-16 length
16+
check would wave through an astral-plane term — `"👍👍"` is 4 code units but
17+
only 2 codepoints, yields no trigram, and matched every row.
18+
19+
**Breaking for callers passing short terms:** `contains()` calls that previously
20+
returned every row now throw. Terms of 3+ codepoints are unaffected.
21+
22+
`v3FromDriver()` now throws the new `EqlV3CodecError` on a payload that is not
23+
an EQL envelope, instead of surfacing a raw `SyntaxError` for malformed JSON and
24+
passing a bare scalar through unchecked — `v3FromDriver('5')` previously returned
25+
`5` typed as `Encrypted`, which then reached `decrypt` as garbage. The guard
26+
accepts both scalar envelopes (ciphertext at `c`) and SteVec documents
27+
(ciphertext at `sv[0].c`). A SteVec's `sv` must be a non-empty array: `sv[0]` is
28+
the decryption root, so `sv: []` carries a ciphertext key but no ciphertext, and
29+
is now rejected rather than passed to `decrypt`. `EqlV3CodecError` is exported
30+
from `@cipherstash/stack/eql/v3/drizzle` so callers can catch it.
31+
32+
Also removes an unreachable branch in `inArray`/`notInArray`, whose empty-list
33+
guard already throws before it.
34+
35+
Note: the v2 Drizzle adapter's `like`/`ilike` path builds the same bloom filters
36+
and has the same short-term fail-open. It is **not** fixed here — v2 terms carry
37+
SQL wildcards, so the floor must be measured against what its tokenizer actually
38+
receives before the shared guard can be reused. Tracked separately.

.changeset/eql-v3-drizzle.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
"@cipherstash/stack": minor
3+
---
4+
5+
Add EQL v3 Drizzle support at `@cipherstash/stack/eql/v3/drizzle`. A Drizzle-native
6+
`types` namespace (same PascalCase names as `@cipherstash/stack/eql/v3`) declares
7+
encrypted columns whose Postgres type is the semantic `public.<domain>`; the concrete
8+
type drives the legal query operators. `createEncryptionOperatorsV3` provides
9+
capability-checked `eq`/`ne`/`gt`/`gte`/`lt`/`lte`/`between`/`contains`/`inArray`/
10+
`asc`/`desc`/`and`/`or` that emit the latest two-argument `eql_v3` SQL functions with
11+
full-envelope operands, and
12+
`extractEncryptionSchemaV3` rebuilds the schema for `EncryptionV3`. The existing v2
13+
`@cipherstash/stack/drizzle` integration is unchanged.
14+
15+
The v3 text-search helper is `contains`; obsolete `like`/`ilike` helpers are not
16+
exposed because v3 free-text search is token containment rather than SQL wildcard
17+
matching.

0 commit comments

Comments
 (0)