@cipherstash/stack 0.19.0 → 1.0.0-rc.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +531 -0
- package/README.md +95 -77
- package/dist/adapter-kit.cjs +923 -0
- package/dist/adapter-kit.cjs.map +1 -0
- package/dist/adapter-kit.d.cts +82 -0
- package/dist/adapter-kit.d.ts +82 -0
- package/dist/adapter-kit.js +47 -0
- package/dist/adapter-kit.js.map +1 -0
- package/dist/base-operation-AOAIvsSB.d.cts +32 -0
- package/dist/base-operation-FXEzUXIq.d.ts +32 -0
- package/dist/{chunk-4AVL4VZD.js → chunk-3B5ZX3IS.js} +3 -1
- package/dist/chunk-3B5ZX3IS.js.map +1 -0
- package/dist/chunk-7333ZC6L.js +48 -0
- package/dist/chunk-7333ZC6L.js.map +1 -0
- package/dist/{chunk-MP3SSDNN.js → chunk-CLM7E4I6.js} +15 -15
- package/dist/{chunk-MP3SSDNN.js.map → chunk-CLM7E4I6.js.map} +1 -1
- package/dist/{chunk-U66S7VIF.js → chunk-V2Q3NYIH.js} +166 -107
- package/dist/chunk-V2Q3NYIH.js.map +1 -0
- package/dist/chunk-X3JRXEIB.js +98 -0
- package/dist/chunk-X3JRXEIB.js.map +1 -0
- package/dist/{chunk-36AA7IBJ.js → chunk-ZTP5QJ27.js} +51 -18
- package/dist/chunk-ZTP5QJ27.js.map +1 -0
- package/dist/client.cjs +29 -12
- package/dist/client.cjs.map +1 -1
- package/dist/client.d.cts +3 -2
- package/dist/client.d.ts +3 -2
- package/dist/client.js +2 -2
- package/dist/{table-DihEAlxG.d.cts → columns-D2_YzrCX.d.cts} +155 -136
- package/dist/{table-CIH7jZ2h.d.ts → columns-rZc7fQHI.d.ts} +155 -136
- package/dist/dynamodb/index.d.cts +3 -2
- package/dist/dynamodb/index.d.ts +3 -2
- package/dist/encryption/index.cjs +54 -16
- package/dist/encryption/index.cjs.map +1 -1
- package/dist/encryption/index.d.cts +807 -6
- package/dist/encryption/index.d.ts +807 -6
- package/dist/encryption/index.js +4 -4
- package/dist/encryption/v3.cjs +1078 -973
- package/dist/encryption/v3.cjs.map +1 -1
- package/dist/encryption/v3.d.cts +15 -12
- package/dist/encryption/v3.d.ts +15 -12
- package/dist/encryption/v3.js +49 -21
- package/dist/encryption/v3.js.map +1 -1
- package/dist/eql/v3/index.cjs +130 -79
- package/dist/eql/v3/index.cjs.map +1 -1
- package/dist/eql/v3/index.d.cts +115 -13
- package/dist/eql/v3/index.d.ts +115 -13
- package/dist/eql/v3/index.js +16 -6
- package/dist/index-BquA71_Y.d.ts +24 -0
- package/dist/index-fhWTOV0K.d.cts +24 -0
- package/dist/index.cjs +79 -27
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -17
- package/dist/index.d.ts +5 -17
- package/dist/index.js +4 -4
- package/dist/schema/index.cjs +29 -12
- package/dist/schema/index.cjs.map +1 -1
- package/dist/schema/index.d.cts +1 -1
- package/dist/schema/index.d.ts +1 -1
- package/dist/schema/index.js +2 -2
- package/dist/{types-public-CpS5KjwX.d.ts → types-public-QMjYNfQO.d.cts} +765 -726
- package/dist/{types-public-CpS5KjwX.d.cts → types-public-QMjYNfQO.d.ts} +765 -726
- package/dist/types-public.cjs.map +1 -1
- package/dist/types-public.d.cts +1 -1
- package/dist/types-public.d.ts +1 -1
- package/dist/types-public.js +1 -1
- package/dist/wasm-inline.d.ts +779 -405
- package/dist/wasm-inline.js +606 -299
- package/dist/wasm-inline.js.map +1 -1
- package/package.json +25 -53
- package/dist/chunk-36AA7IBJ.js.map +0 -1
- package/dist/chunk-4AVL4VZD.js.map +0 -1
- package/dist/chunk-IADZCZEA.js +0 -23
- package/dist/chunk-IADZCZEA.js.map +0 -1
- package/dist/chunk-IBSK6P33.js +0 -209
- package/dist/chunk-IBSK6P33.js.map +0 -1
- package/dist/chunk-U66S7VIF.js.map +0 -1
- package/dist/client-DSGHBN-g.d.cts +0 -834
- package/dist/client-DfCrlHXh.d.ts +0 -834
- package/dist/drizzle/index.cjs +0 -5617
- package/dist/drizzle/index.cjs.map +0 -1
- package/dist/drizzle/index.d.cts +0 -358
- package/dist/drizzle/index.d.ts +0 -358
- package/dist/drizzle/index.js +0 -1220
- package/dist/drizzle/index.js.map +0 -1
- package/dist/supabase/index.cjs +0 -5951
- package/dist/supabase/index.cjs.map +0 -1
- package/dist/supabase/index.d.cts +0 -223
- package/dist/supabase/index.d.ts +0 -223
- package/dist/supabase/index.js +0 -1217
- package/dist/supabase/index.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,536 @@
|
|
|
1
1
|
# @cipherstash/stack
|
|
2
2
|
|
|
3
|
+
## 1.0.0-rc.0
|
|
4
|
+
|
|
5
|
+
### Major Changes
|
|
6
|
+
|
|
7
|
+
- 7c7dbca: CipherStash Stack 1.0 (release candidate).
|
|
8
|
+
|
|
9
|
+
This is the first 1.0-line release of `@cipherstash/stack`, the first published
|
|
10
|
+
release of the split-out EQL v3 adapters `@cipherstash/stack-drizzle` and
|
|
11
|
+
`@cipherstash/stack-supabase`, and moves the `stash` CLI to 1.0 alongside them.
|
|
12
|
+
These four packages now version together as the Stack 1.0 family.
|
|
13
|
+
|
|
14
|
+
### Minor Changes
|
|
15
|
+
|
|
16
|
+
- 31ca318: Split the Drizzle and Supabase integrations into their own packages.
|
|
17
|
+
|
|
18
|
+
The adapters now ship as first-party packages that depend on `@cipherstash/stack`,
|
|
19
|
+
following the `@cipherstash/prisma-next` precedent:
|
|
20
|
+
|
|
21
|
+
- **`@cipherstash/stack-drizzle`** — Drizzle ORM integration. EQL v2 on the package
|
|
22
|
+
root (`@cipherstash/stack-drizzle`: `encryptedType`, `extractEncryptionSchema`,
|
|
23
|
+
`createEncryptionOperators`) and EQL v3 on `@cipherstash/stack-drizzle/v3`
|
|
24
|
+
(`types` factories, `createEncryptionOperatorsV3`, `extractEncryptionSchemaV3`, …).
|
|
25
|
+
- **`@cipherstash/stack-supabase`** — Supabase integration: `encryptedSupabase` (v2)
|
|
26
|
+
and `encryptedSupabaseV3` (v3, connect-time introspection).
|
|
27
|
+
|
|
28
|
+
**Breaking (`@cipherstash/stack`):** the `./drizzle`, `./supabase`, and
|
|
29
|
+
`./eql/v3/drizzle` subpath exports are removed. Migrate imports:
|
|
30
|
+
|
|
31
|
+
- `@cipherstash/stack/drizzle` → `@cipherstash/stack-drizzle`
|
|
32
|
+
- `@cipherstash/stack/eql/v3/drizzle` → `@cipherstash/stack-drizzle/v3`
|
|
33
|
+
- `@cipherstash/stack/supabase` → `@cipherstash/stack-supabase`
|
|
34
|
+
|
|
35
|
+
Add the relevant package to your dependencies alongside `@cipherstash/stack`. A new
|
|
36
|
+
`@cipherstash/stack/adapter-kit` subpath exposes the narrow core internals the
|
|
37
|
+
first-party adapters consume; it is the core↔adapter seam, not general-purpose API.
|
|
38
|
+
|
|
39
|
+
- c4787c0: Restore the EQL v3 envelope and `Result` types the adapters were erasing.
|
|
40
|
+
|
|
41
|
+
Both v3 adapters typed their operand-encryption paths as `unknown` and dropped
|
|
42
|
+
the `Result` wrapper, so the query-type encoding and the failure channel were
|
|
43
|
+
invisible to the type system:
|
|
44
|
+
|
|
45
|
+
- `eql/v3/drizzle/operators.ts` typed the client's `encrypt`/`bulkEncrypt` as
|
|
46
|
+
returning `unknown`, collapsed the operation's `Result` to
|
|
47
|
+
`{ data?: unknown; failure?: { message } }`, and cast the bulk response to
|
|
48
|
+
`Array<{ data: unknown }>`.
|
|
49
|
+
- `supabase/query-builder-v3.ts` returned `Promise<unknown[]>` from
|
|
50
|
+
`encryptCollectedTerms`, `bulkEncryptGroup` and `encryptGroupPerTerm`, and the
|
|
51
|
+
base `query-builder.ts` did the same.
|
|
52
|
+
|
|
53
|
+
These now carry the SDK's real types — `Encrypted` (the storage envelope union,
|
|
54
|
+
which includes every v3 per-domain payload), `BulkEncryptedData`, and
|
|
55
|
+
`EncryptedQueryResult` — threaded through a properly-typed operation surface that
|
|
56
|
+
resolves `Result<T, EncryptionError>`. The Supabase divergence the erasure hid is
|
|
57
|
+
now explicit: the v2 path yields `encryptQuery` composite literals and the v3
|
|
58
|
+
path yields `JSON.stringify`'d envelope strings, and both are `EncryptedQueryResult`.
|
|
59
|
+
|
|
60
|
+
Bumped `minor`, not `patch`: `createEncryptionOperatorsV3` is a public export
|
|
61
|
+
(`@cipherstash/stack/eql/v3/drizzle`), and tightening its client contract from
|
|
62
|
+
`unknown` to a typed operation surface is a compile-time breaking change — a
|
|
63
|
+
downstream consumer passing a loosely-typed (`unknown`-returning) client double
|
|
64
|
+
will now fail `tsc`. That tightening has teeth: `operators.test-d.ts` pins it
|
|
65
|
+
with a negative type-test asserting an `unknown`-returning `{ encrypt }` double
|
|
66
|
+
is rejected (a positive "correctly-typed double is accepted" assertion cannot
|
|
67
|
+
catch a re-erasure, since a correct value is assignable to `unknown`).
|
|
68
|
+
|
|
69
|
+
Behaviour is otherwise unchanged, with one addition: the Supabase v3 bulk path
|
|
70
|
+
now rejects a `null` envelope returned by `bulkEncrypt` (the restored
|
|
71
|
+
`Encrypted | null` type makes that arm reachable, and a `null` would otherwise
|
|
72
|
+
be `JSON.stringify`'d to the literal `"null"` and sent as a filter operand).
|
|
73
|
+
|
|
74
|
+
- 66a0e02: Add the EQL v3 bigint domain family to the public DSL: `types.Bigint`,
|
|
75
|
+
`types.BigintEq`, `types.BigintOrdOre`, and `types.BigintOrd`, backed by the
|
|
76
|
+
`public.bigint*` concrete domains. Plaintext is a JS `bigint`, round-tripped
|
|
77
|
+
losslessly across the protect-ffi 0.28 boundary (i64 bounds enforced at the
|
|
78
|
+
FFI — out-of-range values surface as encryption errors). Index emission follows
|
|
79
|
+
the numeric rule: `bigint_eq` → unique (hm); `bigint_ord`/`bigint_ord_ore` →
|
|
80
|
+
ore (equality answered via ob).
|
|
81
|
+
- 7eba32d: EQL v3 Drizzle: encrypt every query operand with `encryptQuery`, not `encrypt` (#622).
|
|
82
|
+
|
|
83
|
+
The v3 Drizzle operators (`eq`/`ne`/`gt`/`gte`/`lt`/`lte`/`between`/`notBetween`/
|
|
84
|
+
`inArray`/`notInArray`/`contains`) previously encrypted their operands with
|
|
85
|
+
`client.encrypt`, producing a full storage envelope (including the ciphertext `c`)
|
|
86
|
+
cast to `::jsonb`. A WHERE-clause operand should be a query _term_, not a value to
|
|
87
|
+
store. Every operator now uses `client.encryptQuery`, which yields a
|
|
88
|
+
ciphertext-free query term cast to the column's `eql_v3.query_<domain>` type — so
|
|
89
|
+
predicates carry no ciphertext and reach the bundle's `(domain, query_<domain>)`
|
|
90
|
+
operator overloads. This unifies the scalar/text operators with the JSON
|
|
91
|
+
containment path (already on `encryptQuery`) and removes the previously-optional
|
|
92
|
+
`encryptQuery` guard: it is now a required capability of the operand client.
|
|
93
|
+
|
|
94
|
+
`@cipherstash/stack` gains a batch `encryptQuery(terms)` overload on
|
|
95
|
+
`TypedEncryptionClient` (the type `EncryptionV3` returns), mirroring the nominal
|
|
96
|
+
`EncryptionClient`. This is additive — it lets `inArray`/`notInArray` encrypt a
|
|
97
|
+
whole list of query terms in one crossing.
|
|
98
|
+
|
|
99
|
+
- 0ebf57e: Close two fail-open paths in the EQL v3 Drizzle adapter.
|
|
100
|
+
|
|
101
|
+
`ops.contains()` now throws `EncryptionOperatorError` for a search term that
|
|
102
|
+
tokenizes to nothing: the empty string, or a term shorter than the match index
|
|
103
|
+
tokenizer's `token_length` (3 by default). Such a term produces an empty bloom
|
|
104
|
+
filter, and `stored_bf @> '{}'` is true for every row — so a user searching
|
|
105
|
+
`"ad"` silently received the entire table. Measured live, the terms `"ad"`,
|
|
106
|
+
`"a"` and `"x"` each returned every seeded row, including one in which `"x"`
|
|
107
|
+
did not appear.
|
|
108
|
+
|
|
109
|
+
The floor counts Unicode codepoints, matching the tokenizer. A UTF-16 length
|
|
110
|
+
check would wave through an astral-plane term — `"👍👍"` is 4 code units but
|
|
111
|
+
only 2 codepoints, yields no trigram, and matched every row.
|
|
112
|
+
|
|
113
|
+
**Breaking for callers passing short terms:** `contains()` calls that previously
|
|
114
|
+
returned every row now throw. Terms of 3+ codepoints are unaffected.
|
|
115
|
+
|
|
116
|
+
`v3FromDriver()` now throws the new `EqlV3CodecError` on a payload that is not
|
|
117
|
+
an EQL envelope, instead of surfacing a raw `SyntaxError` for malformed JSON and
|
|
118
|
+
passing a bare scalar through unchecked — `v3FromDriver('5')` previously returned
|
|
119
|
+
`5` typed as `Encrypted`, which then reached `decrypt` as garbage. The guard
|
|
120
|
+
accepts both scalar envelopes (ciphertext at `c`) and SteVec documents
|
|
121
|
+
(ciphertext at `sv[0].c`). A SteVec's `sv` must be a non-empty array: `sv[0]` is
|
|
122
|
+
the decryption root, so `sv: []` carries a ciphertext key but no ciphertext, and
|
|
123
|
+
is now rejected rather than passed to `decrypt`. `EqlV3CodecError` is exported
|
|
124
|
+
from `@cipherstash/stack/eql/v3/drizzle` so callers can catch it.
|
|
125
|
+
|
|
126
|
+
Also removes an unreachable branch in `inArray`/`notInArray`, whose empty-list
|
|
127
|
+
guard already throws before it.
|
|
128
|
+
|
|
129
|
+
Note: the v2 Drizzle adapter's `like`/`ilike` path builds the same bloom filters
|
|
130
|
+
and has the same short-term fail-open. It is **not** fixed here — v2 terms carry
|
|
131
|
+
SQL wildcards, so the floor must be measured against what its tokenizer actually
|
|
132
|
+
receives before the shared guard can be reused. Tracked separately.
|
|
133
|
+
|
|
134
|
+
- d73a03c: Add EQL v3 Drizzle support at `@cipherstash/stack/eql/v3/drizzle`. A Drizzle-native
|
|
135
|
+
`types` namespace (same PascalCase names as `@cipherstash/stack/eql/v3`) declares
|
|
136
|
+
encrypted columns whose Postgres type is the semantic `public.<domain>`; the concrete
|
|
137
|
+
type drives the legal query operators. `createEncryptionOperatorsV3` provides
|
|
138
|
+
capability-checked `eq`/`ne`/`gt`/`gte`/`lt`/`lte`/`between`/`contains`/`inArray`/
|
|
139
|
+
`asc`/`desc`/`and`/`or` that emit the latest two-argument `eql_v3` SQL functions with
|
|
140
|
+
full-envelope operands, and
|
|
141
|
+
`extractEncryptionSchemaV3` rebuilds the schema for `EncryptionV3`. The existing v2
|
|
142
|
+
`@cipherstash/stack/drizzle` integration is unchanged.
|
|
143
|
+
|
|
144
|
+
The v3 text-search helper is `contains`; obsolete `like`/`ilike` helpers are not
|
|
145
|
+
exposed because v3 free-text search is token containment rather than SQL wildcard
|
|
146
|
+
matching.
|
|
147
|
+
|
|
148
|
+
- 89b903f: Upgrade `@cipherstash/protect-ffi` to 0.28.0 and update EQL v3 concrete Postgres domain names to match the SQL fixture (`integer*`, `smallint*`, `bool`, `real*`, and `double*`). The public factories remain semantic (`Integer`, `Smallint`, `Boolean`, `Real`, `Double`) while their concrete domains change, so this is a minor release.
|
|
149
|
+
- 229ce59: Re-baseline EQL v3 on the eql-3.0.0 GA release and protect-ffi 0.29.
|
|
150
|
+
|
|
151
|
+
- **Breaking (v3 preview surface):** the EQL v3 column domains follow the
|
|
152
|
+
eql-3.0.0 naming convention — flat, prefixed names in `public`
|
|
153
|
+
(`public.eql_v3_text_search`, `public.eql_v3_integer_ord`, …) instead of the
|
|
154
|
+
alpha-era bare names. Databases installed from an alpha bundle must be
|
|
155
|
+
re-installed (`stash eql install --eql-version 3` replaces the schema).
|
|
156
|
+
- `encryptQuery` under `eqlVersion: 3` now returns EQL v3 query operands
|
|
157
|
+
(protect-ffi 0.29): term-only scalar operands for the `eql_v3.query_<name>`
|
|
158
|
+
domains, the `eql_v3.query_jsonb` containment needle, and bare selector
|
|
159
|
+
hashes for JSON path queries — v3 scalar and selector queries no longer
|
|
160
|
+
throw `EQL_V3_QUERY_UNSUPPORTED` (the code is gone).
|
|
161
|
+
- v2 `searchableJson()` columns now pin the SteVec encoding to `standard`
|
|
162
|
+
explicitly. protect-ffi 0.29 flipped the library default to `compat`
|
|
163
|
+
(EQL v3's encoding); without the pin, v2 JSON containment queries would
|
|
164
|
+
silently match nothing and newly written rows would not be comparable with
|
|
165
|
+
existing ones.
|
|
166
|
+
- The EQL v3 test/install SQL is sourced from the pinned `@cipherstash/eql`
|
|
167
|
+
package (3.0.0) instead of a hand-vendored fixture.
|
|
168
|
+
|
|
169
|
+
- 50c0a9c: Add EQL v3 JSON columns. `types.Json('col')` declares a `public.eql_v3_json`
|
|
170
|
+
column that encrypts a JSON document to an ste_vec `SteVecDocument` and
|
|
171
|
+
round-trips it losslessly through `encrypt`/`decrypt` and the model path. A new
|
|
172
|
+
`searchableJson` query capability emits the ste_vec index; the index uses
|
|
173
|
+
`mode: 'compat'`, which eql-3.0.0's `eql_v3_json` requires (it orders ste_vec
|
|
174
|
+
entries by the CLLW-OPE `op` term, so v2's `'standard'`/CLLW-`oc` terms are
|
|
175
|
+
rejected).
|
|
176
|
+
|
|
177
|
+
The Drizzle integration's `contains(col, subObject)` now answers encrypted-JSONB
|
|
178
|
+
containment on a `types.Json` column, emitting the `@>` operator with a
|
|
179
|
+
`query_jsonb` needle (from `encryptQuery`). The ste_vec index indexes array
|
|
180
|
+
elements by identity but not position, so containment is a true subset test
|
|
181
|
+
(`{ roles: ['x'] }` matches any document whose `roles` array contains `x`,
|
|
182
|
+
regardless of index).
|
|
183
|
+
|
|
184
|
+
- 5d23e80: Add `encryptedSupabaseV3` — the EQL v3 dialect of the Supabase adapter. It is
|
|
185
|
+
now a connect-time-async factory: `await encryptedSupabaseV3(url, key)` (or
|
|
186
|
+
`(client)`) introspects the database over `DATABASE_URL`, detects EQL v3 columns
|
|
187
|
+
by their Postgres domain (`information_schema.columns.domain_name`), and derives
|
|
188
|
+
each column's encryption config from its domain — callers no longer pass a
|
|
189
|
+
schema to `from()`. `select('*')` is supported (expanded from the introspected
|
|
190
|
+
column list, and aliased back to each declared column's JS property name so a
|
|
191
|
+
property→DB rename round-trips). A column using an EQL v3 domain this SDK version does not model
|
|
192
|
+
(e.g. `public.json`, `*_ord_ope`) throws at construction rather than silently
|
|
193
|
+
passing through. Supplying `schemas` remains optional and adds compile-time
|
|
194
|
+
types plus startup verification of the declared tables against the database.
|
|
195
|
+
Requires a Postgres connection for introspection (`pg` is a new optional peer),
|
|
196
|
+
so it cannot run in a Worker or the browser.
|
|
197
|
+
|
|
198
|
+
Every column name a query carries — filters, `match`, `not`, raw `filter`,
|
|
199
|
+
`or()`, `order()`, and the `onConflict` option — is now resolved from its JS
|
|
200
|
+
property name to its DB column name in a single pass before the query is built,
|
|
201
|
+
so a declared rename round-trips everywhere rather than only on the paths that
|
|
202
|
+
remembered to translate.
|
|
203
|
+
|
|
204
|
+
`order()` on ANY encrypted v3 column is now rejected — at compile time when
|
|
205
|
+
`schemas` is supplied, and at runtime otherwise. The EQL v3 domains are
|
|
206
|
+
`DOMAIN … AS jsonb` and the bundle declares no btree operator class on them, so
|
|
207
|
+
`ORDER BY col` resolves through jsonb's default `jsonb_cmp` and sorts by the
|
|
208
|
+
envelope's byte structure: a stable, plausible-looking, meaningless row order,
|
|
209
|
+
with no error. Correct ordering is `ORDER BY eql_v3.ord_term(col)`, which
|
|
210
|
+
PostgREST's `order=` cannot express. Order by a plaintext column, expose
|
|
211
|
+
`eql_v3.ord_term()` as a generated column or view, or use the EQL v3 Drizzle
|
|
212
|
+
integration, which emits `ord_term` directly. Note `gte`/`lte` filters remain
|
|
213
|
+
correct: the comparison operators _are_ declared on the ord domains, and only
|
|
214
|
+
sorting resolves through the missing operator class.
|
|
215
|
+
|
|
216
|
+
`.or()` now understands PostgREST's `column.not.<op>.<value>` negation. It was
|
|
217
|
+
previously parsed as `{ op: 'not', value: '<op>.<value>' }`, so on an encrypted
|
|
218
|
+
column `or('nickname.not.in.(ada,grace)')` encrypted the literal string
|
|
219
|
+
`in.(ada,grace)` as a single plaintext and produced a filter that silently
|
|
220
|
+
matched nothing.
|
|
221
|
+
|
|
222
|
+
Free-text search on the v3 builder is `contains(column, value)`. `like`/`ilike`
|
|
223
|
+
are not exposed, because EQL v3 free-text search is token containment over a
|
|
224
|
+
bloom filter (`@>`, backed by `eql_v3.contains`) rather than SQL wildcard
|
|
225
|
+
matching — `%` is tokenized like any other character, so a `like` pattern is a
|
|
226
|
+
category error. This matches the v3 Drizzle integration, which omits them for
|
|
227
|
+
the same reason. On an encrypted column `like`/`ilike` now throw and name
|
|
228
|
+
`contains`; on a plaintext column they remain ordinary PostgREST filters.
|
|
229
|
+
|
|
230
|
+
`contains` is narrowed at compile time to columns whose domain carries the
|
|
231
|
+
`freeTextSearch` capability (`public.text_match`, `public.text_search`), and
|
|
232
|
+
guarded at runtime for the untyped surface. A raw `filter(column, operator, …)`
|
|
233
|
+
on an encrypted v3 column now derives its query type from the operator instead
|
|
234
|
+
of always encrypting an equality term, so `filter('bio', 'cs', …)` on a
|
|
235
|
+
`public.text_match` column works rather than being rejected, and an unsupported
|
|
236
|
+
operator throws instead of silently encrypting the wrong term.
|
|
237
|
+
|
|
238
|
+
Substring `contains` matches any needle whose trigrams are all present in the
|
|
239
|
+
stored value; needles shorter than the tokenizer's window (3 characters) bloom to
|
|
240
|
+
nothing and are rejected rather than silently matching every row. The v3 match
|
|
241
|
+
index now emits `include_original: false` — the flag is inert in protect-ffi (the
|
|
242
|
+
bloom is trigram-only either way), so this moves no ciphertext and only pins the
|
|
243
|
+
value a substring-search domain wants.
|
|
244
|
+
|
|
245
|
+
v2 (`encryptedSupabase`) is unchanged: it keeps `like`/`ilike` (`eql_v2.like`,
|
|
246
|
+
`~~`) and its raw-`filter` query-type mapping, so no v2 ciphertext moves.
|
|
247
|
+
|
|
248
|
+
- 1aa9a11: Add the EQL v3 `text_search` authoring DSL on a new `@cipherstash/stack/eql/v3`
|
|
249
|
+
subpath (`types.TextSearch`, v3 `encryptedTable` / `buildEncryptConfig`). The v3
|
|
250
|
+
builders emit the existing `EncryptConfig` shape, so encryption, payloads, and
|
|
251
|
+
query paths are unchanged at runtime.
|
|
252
|
+
|
|
253
|
+
Also widens the public client types (`EncryptionClientConfig.schemas`,
|
|
254
|
+
`EncryptOptions`, `SearchTerm`/`EncryptQueryOptions`) to a structural contract so
|
|
255
|
+
both v2 and v3 builders are accepted by `Encryption` / `encrypt` / `decrypt` /
|
|
256
|
+
`encryptQuery`. This is a backward-compatible widening — existing v2 usage is
|
|
257
|
+
unaffected. The structural contracts themselves (`BuildableColumn`,
|
|
258
|
+
`BuildableQueryColumn`, `BuildableV3QueryableColumn`, `BuildableTable`,
|
|
259
|
+
`BuildableTableColumns`) and the `encryptModel` return-type mapper
|
|
260
|
+
(`EncryptedFromBuildableTable`) are exported from `@cipherstash/stack/types` so
|
|
261
|
+
consumers can name them.
|
|
262
|
+
|
|
263
|
+
- af2d04e: Add a strongly-typed EQL v3 client surface on a new `@cipherstash/stack/v3`
|
|
264
|
+
subpath (`EncryptionV3`, `typedClient`, `TypedEncryptionClient`). It re-exports
|
|
265
|
+
the v3 `types` namespace and table API (from `@cipherstash/stack/eql/v3`), so a
|
|
266
|
+
single import provides everything needed to author and use a v3 schema.
|
|
267
|
+
|
|
268
|
+
Every method derives its types from the concrete `table` / `column` builder
|
|
269
|
+
arguments:
|
|
270
|
+
|
|
271
|
+
- `encrypt` / `encryptQuery` pin the plaintext to the column's domain type
|
|
272
|
+
(`text → string`, `timestamp → Date`, …).
|
|
273
|
+
- `encryptQuery` constrains `queryType` to the column's capabilities and rejects
|
|
274
|
+
storage-only columns at compile time.
|
|
275
|
+
- `encryptModel` / `bulkEncryptModels` validate schema-column fields against their
|
|
276
|
+
inferred plaintext type (passthrough fields are untouched) and return a precise
|
|
277
|
+
encrypted model.
|
|
278
|
+
- `decryptModel` / `bulkDecryptModels` return the precise plaintext model,
|
|
279
|
+
reconstructing `Date` values from the encrypt-config `cast_as`.
|
|
280
|
+
|
|
281
|
+
Because the typed methods bind to the concrete branded v3 classes, a hand-rolled
|
|
282
|
+
structural table/column is rejected — closing the soundness gap where a non-branded
|
|
283
|
+
table could be encrypted at runtime while typed as plaintext.
|
|
284
|
+
|
|
285
|
+
Runtime behaviour is unchanged: the encrypt/query paths return the same operations
|
|
286
|
+
as the base client; only the model-decrypt paths add a per-column `Date`
|
|
287
|
+
reconstruction step. The v2 client surface (`Encryption`) is untouched.
|
|
288
|
+
|
|
289
|
+
- b8a3d20: Add EQL v3 schema builders for supported generated SQL domains under `@cipherstash/stack/eql/v3`, exposed as the `types` namespace (one member per supported EQL v3 domain, e.g. `types.TextEq` / `types.IntegerOrd` / `types.Timestamp`), including explicit query capability metadata (`getQueryCapabilities()` / `isQueryable()`) and v3 table support in model encryption helpers (`encryptModel` / `bulkEncryptModels`).
|
|
290
|
+
|
|
291
|
+
Also widen the accepted plaintext input type for `encrypt` / `encryptQuery` to include `Date` (via the new `Plaintext` type), so v3 `date` / `timestamp` domains can be encrypted and queried with their natural JavaScript values.
|
|
292
|
+
|
|
293
|
+
- a0f3b2c: `@cipherstash/stack/wasm-inline` is now EQL v3 (#614).
|
|
294
|
+
|
|
295
|
+
The WASM entry (Deno / Bun / Cloudflare Workers / Supabase Edge) previously
|
|
296
|
+
created a client pinned to the FFI's EQL v2 wire format, so a v3 schema
|
|
297
|
+
(concrete `eql_v3_*` domains) failed every encrypt on the edge. It now targets
|
|
298
|
+
EQL v3 exclusively:
|
|
299
|
+
|
|
300
|
+
- The factory constructs the WASM client with `eqlVersion: 3`, so v3 schemas
|
|
301
|
+
encrypt/decrypt correctly on the edge.
|
|
302
|
+
- The entry re-exports the **v3** authoring surface (`types`, `encryptedTable`,
|
|
303
|
+
the column classes, `buildEncryptConfig`, and the inference helpers) — the
|
|
304
|
+
same API as `@cipherstash/stack/eql/v3` — so an Edge Function authors and runs
|
|
305
|
+
v3 from one import:
|
|
306
|
+
|
|
307
|
+
```ts
|
|
308
|
+
import {
|
|
309
|
+
Encryption,
|
|
310
|
+
encryptedTable,
|
|
311
|
+
types,
|
|
312
|
+
} from "@cipherstash/stack/wasm-inline";
|
|
313
|
+
|
|
314
|
+
const patients = encryptedTable("patients", {
|
|
315
|
+
email: types.TextSearch("email"),
|
|
316
|
+
});
|
|
317
|
+
const client = await Encryption({ schemas: [patients], config });
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
The v2 schema builders (`encryptedColumn` / `encryptedField` / the v2
|
|
321
|
+
`encryptedTable`) are no longer exported from this entry, and passing a v2 table
|
|
322
|
+
throws a clear error. The WASM path was never announced or documented for v2 and
|
|
323
|
+
had no known users; EQL v2 remains fully supported on the native
|
|
324
|
+
`@cipherstash/stack` entry.
|
|
325
|
+
|
|
326
|
+
- 5411a13: Add the `@cipherstash/stack/adapter-kit` subpath — a narrow support surface for
|
|
327
|
+
the first-party adapter packages (`@cipherstash/stack-supabase`,
|
|
328
|
+
`@cipherstash/stack-drizzle`) being split out of this package (#627). It
|
|
329
|
+
re-exports exactly the core internals the adapters consume (the logger,
|
|
330
|
+
`AuditConfig`, the v3 column model + `DATE_LIKE_CASTS`, the domain registry, the
|
|
331
|
+
match-index guard, and the model→composite helpers) so those imports resolve
|
|
332
|
+
across the package boundary without leaking six internal module paths. This is the
|
|
333
|
+
core↔adapter seam, not general-purpose public API.
|
|
334
|
+
- 99f8b0a: Fix encrypted `in`-list operands in the Supabase adapter, and widen the `is` /
|
|
335
|
+
`contains` type surfaces.
|
|
336
|
+
|
|
337
|
+
**`in()` on an encrypted column produced a request PostgREST rejects.** Every
|
|
338
|
+
encrypted operand is a serialized envelope, dense with `"` and `,`. postgrest-js
|
|
339
|
+
wraps a comma-bearing element as `"…"` but never escapes the quotes already
|
|
340
|
+
inside it, so `.in('email', […])` emitted
|
|
341
|
+
|
|
342
|
+
```
|
|
343
|
+
in.("{"v":1,"c":"…"}",…)
|
|
344
|
+
^ PostgREST ends the value here → PGRST100
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
Encrypted lists are now emitted through `filter(col, 'in', …)` with each element
|
|
348
|
+
quoted and escaped, matching what the `.or()` path already did. This affects
|
|
349
|
+
**v2 as well as v3** — v2's `("a@b.com")` composite literal is itself
|
|
350
|
+
quote-bearing and was equally broken.
|
|
351
|
+
|
|
352
|
+
**`not(col, 'in', […])` encrypted the whole list as a single ciphertext**, so
|
|
353
|
+
the filter silently matched nothing, and emitted an unparenthesized
|
|
354
|
+
`not.in.a,b`. Each element is now encrypted separately and the operand is
|
|
355
|
+
rendered as `not.in.(…)`. Passing a PostgREST list literal (`'(a,b)'`) for an
|
|
356
|
+
encrypted column now throws instead of silently matching nothing — pass an
|
|
357
|
+
array.
|
|
358
|
+
|
|
359
|
+
**`filter(col, 'in', […])` encrypted the whole list as a single ciphertext.**
|
|
360
|
+
The raw `.filter()` path reached `in` with none of the element-splitting the
|
|
361
|
+
`in()`, `not(…, 'in', …)` and `.or()` paths perform, so the entire list operand
|
|
362
|
+
was encrypted as one equality term. The two wire formats then failed
|
|
363
|
+
differently, which is why this went unnoticed: **v2**'s `("json")` composite
|
|
364
|
+
literal is already parenthesized, so PostgREST parsed it as a one-element list
|
|
365
|
+
and answered `200 []` — a filter that silently matched nothing. **v3**'s bare
|
|
366
|
+
`{…}` envelope is not, so PostgREST rejected the request outright with
|
|
367
|
+
`PGRST100 (failed to parse filter)`.
|
|
368
|
+
|
|
369
|
+
Each element is now encrypted separately and the operand rendered as a quoted
|
|
370
|
+
PostgREST list literal. As on the `not` path, passing a list literal
|
|
371
|
+
(`'(a,b)'`) for an encrypted column now throws instead — pass an array.
|
|
372
|
+
|
|
373
|
+
Plaintext columns are unaffected, including the pre-existing quirk that
|
|
374
|
+
postgrest-js renders `.filter(col, 'in', [array])` as an unparenthesized
|
|
375
|
+
`in.a,b` that PostgREST rejects; pass a list literal there, or use `.in()`.
|
|
376
|
+
|
|
377
|
+
**`is(col, null)` is now allowed on every column**, including storage-only
|
|
378
|
+
encrypted ones (`types.Boolean`, `types.Integer`, …). `is` is never encrypted
|
|
379
|
+
and a NULL plaintext is stored as a SQL NULL, so `IS NULL` is not merely legal
|
|
380
|
+
there but the only predicate those columns support. `is(col, true)` remains a
|
|
381
|
+
compile error on encrypted columns.
|
|
382
|
+
|
|
383
|
+
**`contains()` accepts native operands on plaintext array and jsonb columns.** A
|
|
384
|
+
plaintext jsonb/array column falls through to PostgREST's native containment, so
|
|
385
|
+
`contains('tags', ['vip'])` and `contains('meta', { plan: 'pro' })` now
|
|
386
|
+
typecheck. A plaintext SCALAR column does not: `@>` is undefined on `text`, so
|
|
387
|
+
the operand type follows the column's own shape and a scalar rejects every
|
|
388
|
+
containment operand. Encrypted match columns still take a `string` token.
|
|
389
|
+
Relatedly, `.or([{ op: 'contains' }])` now emits PostgREST's `cs` operator for
|
|
390
|
+
plaintext columns too — previously only encrypted conditions were translated, so
|
|
391
|
+
a plaintext containment reached the wire as `.contains.` and failed to parse.
|
|
392
|
+
|
|
393
|
+
**Direct `contains()` / `not(col, 'contains', …)` now serialize their operand.**
|
|
394
|
+
postgrest-js builds an array operand as `cs.{a,b}` with no element quoting, so
|
|
395
|
+
`contains('tags', ['with,comma'])` reached Postgres as two elements; and its
|
|
396
|
+
`not()` stringifies the operand outright, emitting `not.contains.with,comma`
|
|
397
|
+
(no braces, and the wrong operator token) or `[object Object]` for a jsonb
|
|
398
|
+
operand. Both paths now build the containment literal the `.or()` path already
|
|
399
|
+
built, and emit the `cs` token.
|
|
400
|
+
|
|
401
|
+
**`.or()` no longer drops a condition after an unbalanced brace or paren.** A
|
|
402
|
+
scalar operand containing `{` left the parser's depth counter stranded above
|
|
403
|
+
zero, so no later comma separated a condition and everything behind it was
|
|
404
|
+
swallowed into that operand. With a plaintext column first, the group was then
|
|
405
|
+
forwarded verbatim — running the swallowed condition against a ciphertext column
|
|
406
|
+
with a plaintext operand. Braces are now quoted on emit (they are structural to
|
|
407
|
+
PostgREST inside `or=(…)`), and the parser falls back to quote-only splitting
|
|
408
|
+
when its depth tracking does not balance.
|
|
409
|
+
|
|
410
|
+
**`is(col, true)` is now rejected on every encrypted column, not just the
|
|
411
|
+
storage-only ones.** The boolean form was gated on the filterable keys, which
|
|
412
|
+
exclude storage-only columns but keep queryable encrypted ones — so
|
|
413
|
+
`is(emailTextSearchColumn, true)` compiled and emitted `IS TRUE` against a jsonb
|
|
414
|
+
ciphertext.
|
|
415
|
+
|
|
416
|
+
**In-list operands encrypt in one crossing per column.** The element-wise `in` /
|
|
417
|
+
`not.in` encoding above spent one ZeroKMS round-trip per element; terms are now
|
|
418
|
+
grouped by column and each group takes a single `bulkEncrypt` call, matching the
|
|
419
|
+
Drizzle v3 path. Falls back to per-term encryption for clients without
|
|
420
|
+
`bulkEncrypt`, and rejects a bulk response whose length does not match the list
|
|
421
|
+
rather than silently truncating the predicate.
|
|
422
|
+
|
|
423
|
+
- 9b65ae8: **`order()` now works on EQL v3 encrypted ordering columns in the Supabase
|
|
424
|
+
adapter.** It was rejected outright on every encrypted column.
|
|
425
|
+
|
|
426
|
+
A bare `ORDER BY col` on an EQL v3 domain really is wrong — the bundle declares
|
|
427
|
+
no btree operator class on any domain, so the sort falls through to jsonb's
|
|
428
|
+
default `jsonb_cmp` and compares the envelope's keys in storage order, starting
|
|
429
|
+
at the random ciphertext `c`. Measured over ten rows it returns
|
|
430
|
+
`r00,r04,r08,r01,…` where the plaintext order is `r00..r09`. No error, a stable
|
|
431
|
+
and plausible-looking meaningless order.
|
|
432
|
+
|
|
433
|
+
But the correct sort key is reachable without a function call. `eql_v3.ord_term`
|
|
434
|
+
returns the domain's `op` term, and OPE is order-preserving, so ordering by the
|
|
435
|
+
term reproduces the plaintext order. PostgREST cannot emit
|
|
436
|
+
`ORDER BY eql_v3.ord_term(col)`, but it can emit a jsonb path. The builder now
|
|
437
|
+
emits `order=col->op` for an encrypted ordering column, verified against a live
|
|
438
|
+
PostgREST for `integer_ord` and `text_search` in both directions.
|
|
439
|
+
|
|
440
|
+
The guard is now on the ordering FLAVOUR, not on encryption:
|
|
441
|
+
|
|
442
|
+
- **`ope` present → supported.** Every plain `*_ord` domain, plus `text_ord` and
|
|
443
|
+
`text_search`.
|
|
444
|
+
- **`ore` present → rejected.** The `ob` term is an array of ORE blocks whose
|
|
445
|
+
comparison needs the superuser-only operator class, which no jsonb path can
|
|
446
|
+
reach. (Such a column cannot hold data on managed Postgres anyway: its domain
|
|
447
|
+
CHECK raises `ore_domain_unavailable`.) ORE columns are now excluded from
|
|
448
|
+
`order()` at COMPILE time too, not only at runtime — `.order(oreColumn)` is a
|
|
449
|
+
type error, matching the rejection.
|
|
450
|
+
- **neither → rejected.** Storage-only, equality-only and match-only columns
|
|
451
|
+
carry no ordering term.
|
|
452
|
+
|
|
453
|
+
The path is `col->op` (jsonb), not `col->>op` (text). Neither avoids the
|
|
454
|
+
database collation — Postgres compares jsonb strings with `varstr_cmp` under the
|
|
455
|
+
default collation, exactly as it does text. What makes the ordering
|
|
456
|
+
collation-independent is the term's encoding: lowercase hex, fixed-width for
|
|
457
|
+
numeric and date domains, and per-character (16 hex chars each) for text, so
|
|
458
|
+
lexicographic order reproduces plaintext order including the prefix case
|
|
459
|
+
(`ada` < `adam`). `ope-term.integration.test.ts` pins that shape.
|
|
460
|
+
|
|
461
|
+
`V3OrderableKeys` widens to admit OPE-backed ordering columns (`*_ord`,
|
|
462
|
+
`text_ord`, `text_search`) while still excluding ORE (`*_ord_ore`) columns, so
|
|
463
|
+
`order()` typechecks exactly where it works. `is(col, true)` is unaffected — it
|
|
464
|
+
stays plaintext-only, and now has its own `V3PlaintextKeys` rather than
|
|
465
|
+
borrowing the orderable set.
|
|
466
|
+
|
|
467
|
+
### Patch Changes
|
|
468
|
+
|
|
469
|
+
- cfd46ee: 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.
|
|
470
|
+
|
|
471
|
+
Test-and-tooling only — `@cipherstash/eql` is a `devDependency` and no public API changes.
|
|
472
|
+
|
|
473
|
+
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.
|
|
474
|
+
|
|
475
|
+
- 63ca540: Re-vendor the EQL v3 SQL bundle and align the v3 DSL to it: encrypted type domains now live in the `public` schema (`public.text`, `public.integer`, …) rather than `eql_v3`, and the boolean domain is `public.boolean` (was `eql_v3.bool`). The `eql_v3` schema now holds only the operator-backing functions, and the index-term constructors (`hmac_256`, `ore_block_256`, `bloom_filter`) moved to `eql_v3_internal`. This keeps the SDK's emitted domain names byte-matched to the installed bundle so `CREATE TABLE`/cast resolution succeeds.
|
|
476
|
+
- f23f952: Remove the leftovers from the secrets removal (`1929c8fe`), which deleted
|
|
477
|
+
`packages/stack/src/secrets/` but left its export, build entry, skill, and docs
|
|
478
|
+
behind. Secrets tooling is not ready; nothing here was functional.
|
|
479
|
+
|
|
480
|
+
- **Drop the dead `@cipherstash/stack/secrets` subpath export.** It pointed at
|
|
481
|
+
`./dist/secrets/index.js`, which has no source and is not in the tarball, so
|
|
482
|
+
`import '@cipherstash/stack/secrets'` has been throwing `ERR_MODULE_NOT_FOUND`
|
|
483
|
+
for every consumer since the source was removed. Also drops the dangling
|
|
484
|
+
`src/secrets/index.ts` entry from `tsup.config.ts`. Removing an export that
|
|
485
|
+
cannot resolve breaks nothing.
|
|
486
|
+
- **Remove the `stash-secrets` agent skill** and its references in `AGENTS.md`
|
|
487
|
+
and the init setup-prompt skill index. It was never installed by `stash init`
|
|
488
|
+
(it is absent from `SKILL_MAP`), so no user project ever received it.
|
|
489
|
+
- **Remove the secrets documentation** from both published READMEs: the
|
|
490
|
+
`Secrets` class API and the `npx stash secrets` command reference in
|
|
491
|
+
`@cipherstash/stack`, and the `npx stash secrets` section in `stash`. The CLI
|
|
492
|
+
command does not exist — `stash secrets` returns `Unknown command`.
|
|
493
|
+
|
|
494
|
+
- fd33aad: Fix the Supabase adapter encrypting `is` and `null` filter operands.
|
|
495
|
+
|
|
496
|
+
`is` is a SQL predicate — PostgREST accepts only `null`/`true`/`false` after it
|
|
497
|
+
— and a `null` operand is SQL NULL, never a value to search for. Only the direct
|
|
498
|
+
`.is()` filter skipped encryption; `not()`, `or()`, `match()`, raw `filter()`,
|
|
499
|
+
and the `in()` element list all encrypted whatever they were handed. So
|
|
500
|
+
`or('age.is.null')` emitted `age.is."("null")"` and `eq('email', null)` emitted
|
|
501
|
+
`email=("null")` — operands PostgREST rejects. A null plaintext is stored as a
|
|
502
|
+
NULL column rather than ciphertext, so it is found with an unencrypted
|
|
503
|
+
`IS NULL`; encrypting the operand could never match.
|
|
504
|
+
|
|
505
|
+
A single `isEncryptableTerm(operator, value)` predicate now guards every term
|
|
506
|
+
collector. Affects both `encryptedSupabase` (v2) and `encryptedSupabaseV3`. On
|
|
507
|
+
v3 this additionally removes a spurious `does not support equality queries`
|
|
508
|
+
error, which `is` raised because it maps to the `equality` query type and so hit
|
|
509
|
+
the column-capability guard — `or('active.is.null')` on a storage-only column
|
|
510
|
+
threw rather than querying.
|
|
511
|
+
|
|
512
|
+
Relatedly, an `or()` string is now rebuilt whenever a condition _references_ an
|
|
513
|
+
encrypted column, not only when one of its values was encrypted. An `is` on an
|
|
514
|
+
encrypted column encrypts nothing, and the old condition sent it down the
|
|
515
|
+
verbatim path, forwarding the caller's JS property name to a database that only
|
|
516
|
+
knows the column's DB name.
|
|
517
|
+
|
|
518
|
+
- 8cd485d: Fix the Supabase adapter's `.or()` string parser mis-splitting conditions, and pin `contains()` on a mixed union column key to the encrypted operand.
|
|
519
|
+
|
|
520
|
+
An `.or()` string is only rebuilt from its parse when it references an encrypted column — otherwise the caller's string is forwarded verbatim — so each of these corrupts precisely the mixed encrypted/plaintext case.
|
|
521
|
+
|
|
522
|
+
**Quotes were tracked only at brace depth 0.** A `}` inside a quoted array element or jsonb string value closed the literal early, and the next `"` re-opened quoting, so the following top-level comma never split: `.or('tags.cs.{"a}b"},email.eq.secret')` parsed as a single condition and silently absorbed `email.eq.secret` into the operand. Quotes are now opaque at every depth.
|
|
523
|
+
|
|
524
|
+
**A stray `}` or `)` drove the depth counter negative**, after which no comma split again. `}` and `)` are not PostgREST reserved characters, so `a}b` is a valid unquoted operand and `.or('nickname.eq.a}b,id.eq.1')` dropped `id.eq.1`. Depth now floors at zero.
|
|
525
|
+
|
|
526
|
+
**`in`-list elements were split on every comma, ignoring quotes.** `.or('email.in.("a,b",c)')` parsed as three elements with the quotes still embedded; on an encrypted column each fragment was encrypted as its own term, so the intended element never matched. Elements are now split on top-level commas and unquoted, the inverse of what the rebuild emits.
|
|
527
|
+
|
|
528
|
+
**A parenthesized operand was read as a list for every operator.** Only `in` and the range operators (`ov`, `sl`, `sr`, `nxr`, `nxl`, `adj`) take a paren-delimited operand; elsewhere `(` is an ordinary character. `email.eq.(foo)` parsed as `['foo']` and encrypted a JS array rather than the string, matching nothing.
|
|
529
|
+
|
|
530
|
+
**A string operand spelling `null`, `true` or `false` is now quoted.** PostgREST reads a bare `null` as SQL NULL, so `.or([{ column: 'name', op: 'eq', value: 'null' }])` emitted `name.eq.null` and compared against NULL instead of the three-character string.
|
|
531
|
+
|
|
532
|
+
**`contains(col, …)` where `col` is a union spanning an encrypted and a plaintext column** accepted an array or object operand. The union is now only as permissive as its strictest member: any declared encrypted column in the union pins the operand to `string`. A literal column argument was never affected.
|
|
533
|
+
|
|
3
534
|
## 0.19.0
|
|
4
535
|
|
|
5
536
|
### Minor Changes
|