@cipherstash/stack 0.18.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 +704 -0
- package/README.md +97 -79
- 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-VPOTNP4K.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-XWPGAHBZ.js → chunk-BNO32ZMV.js} +1 -1
- package/dist/chunk-BNO32ZMV.js.map +1 -0
- package/dist/chunk-CLM7E4I6.js +4439 -0
- package/dist/chunk-CLM7E4I6.js.map +1 -0
- package/dist/{chunk-LLCCNKDL.js → chunk-HQANMV7R.js} +3 -3
- package/dist/{chunk-LLCCNKDL.js.map → chunk-HQANMV7R.js.map} +1 -1
- package/dist/chunk-JSG2AMDI.js +17 -0
- package/dist/chunk-JSG2AMDI.js.map +1 -0
- package/dist/chunk-OFQ555AX.js +191 -0
- package/dist/chunk-OFQ555AX.js.map +1 -0
- package/dist/chunk-PZ5AY32C.js +10 -0
- package/dist/chunk-PZ5AY32C.js.map +1 -0
- package/dist/chunk-V2Q3NYIH.js +494 -0
- 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-ZYOZHZMJ.js → chunk-ZTP5QJ27.js} +193 -169
- package/dist/chunk-ZTP5QJ27.js.map +1 -0
- package/dist/client.cjs +4124 -49
- package/dist/client.cjs.map +1 -1
- package/dist/client.d.cts +6 -5
- package/dist/client.d.ts +6 -5
- package/dist/client.js +3 -1
- package/dist/columns-D2_YzrCX.d.cts +622 -0
- package/dist/columns-rZc7fQHI.d.ts +622 -0
- package/dist/dynamodb/index.cjs +19 -12
- package/dist/dynamodb/index.cjs.map +1 -1
- package/dist/dynamodb/index.d.cts +3 -2
- package/dist/dynamodb/index.d.ts +3 -2
- package/dist/dynamodb/index.js +8 -10
- package/dist/dynamodb/index.js.map +1 -1
- package/dist/encryption/index.cjs +4496 -222
- 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 +15 -7
- package/dist/encryption/v3.cjs +7896 -0
- package/dist/encryption/v3.cjs.map +1 -0
- package/dist/encryption/v3.d.cts +103 -0
- package/dist/encryption/v3.d.ts +103 -0
- package/dist/encryption/v3.js +189 -0
- package/dist/encryption/v3.js.map +1 -0
- package/dist/eql/v3/index.cjs +663 -0
- package/dist/eql/v3/index.cjs.map +1 -0
- package/dist/eql/v3/index.d.cts +173 -0
- package/dist/eql/v3/index.d.ts +173 -0
- package/dist/eql/v3/index.js +99 -0
- package/dist/eql/v3/index.js.map +1 -0
- package/dist/errors/index.js +1 -0
- package/dist/identity/index.cjs +55 -35
- package/dist/identity/index.cjs.map +1 -1
- package/dist/identity/index.d.cts +54 -30
- package/dist/identity/index.d.ts +54 -30
- package/dist/identity/index.js +9 -177
- package/dist/identity/index.js.map +1 -1
- package/dist/index-BquA71_Y.d.ts +24 -0
- package/dist/index-fhWTOV0K.d.cts +24 -0
- package/dist/index.cjs +4547 -243
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +18 -19
- package/dist/index.d.ts +18 -19
- package/dist/index.js +20 -5
- package/dist/index.js.map +1 -1
- package/dist/schema/index.cjs +4128 -49
- 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 +3 -1
- package/dist/{types-public-BTxqjmtv.d.cts → types-public-QMjYNfQO.d.cts} +448 -251
- package/dist/{types-public-BTxqjmtv.d.ts → types-public-QMjYNfQO.d.ts} +448 -251
- package/dist/types-public.cjs.map +1 -1
- package/dist/types-public.d.cts +2 -2
- package/dist/types-public.d.ts +2 -2
- package/dist/types-public.js +2 -1
- package/dist/wasm-inline.d.ts +1643 -0
- package/dist/wasm-inline.js +4901 -0
- package/dist/wasm-inline.js.map +1 -0
- package/package.json +74 -52
- package/dist/chunk-VPOTNP4K.js.map +0 -1
- package/dist/chunk-XWPGAHBZ.js.map +0 -1
- package/dist/chunk-YWQXTZ5M.js +0 -383
- package/dist/chunk-YWQXTZ5M.js.map +0 -1
- package/dist/chunk-ZYOZHZMJ.js.map +0 -1
- package/dist/client-CIsfZ2zg.d.ts +0 -692
- package/dist/client-Dt98Rugc.d.cts +0 -692
- package/dist/drizzle/index.cjs +0 -1558
- 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 -1215
- package/dist/drizzle/index.js.map +0 -1
- package/dist/supabase/index.cjs +0 -1526
- package/dist/supabase/index.cjs.map +0 -1
- package/dist/supabase/index.d.cts +0 -144
- package/dist/supabase/index.d.ts +0 -144
- package/dist/supabase/index.js +0 -899
- package/dist/supabase/index.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,709 @@
|
|
|
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
|
+
|
|
534
|
+
## 0.19.0
|
|
535
|
+
|
|
536
|
+
### Minor Changes
|
|
537
|
+
|
|
538
|
+
- cc62407: Add EQL v3 Supabase support, baselined on the `eql-3.0.0-alpha.2` release.
|
|
539
|
+
|
|
540
|
+
`@cipherstash/stack/supabase` gains `encryptedSupabaseV3` — the EQL v3
|
|
541
|
+
counterpart of `encryptedSupabase` for schemas authored with
|
|
542
|
+
`@cipherstash/stack/eql/v3`. The public surface and call shape are identical
|
|
543
|
+
to v2 (same filter methods, `withLockContext`, `audit`); only the schema type
|
|
544
|
+
and wire encoding differ.
|
|
545
|
+
|
|
546
|
+
**The v3 surface** is the `eql-3.0.0-alpha.2` release artifact: domains use
|
|
547
|
+
SQL-standard type names (`eql_v3.integer_ord`, `eql_v3.timestamp_ord`,
|
|
548
|
+
`eql_v3.boolean`, … mirrored by `types.IntegerOrd`, `types.TimestampOrd`,
|
|
549
|
+
`types.Boolean`, …), SEM internals live in a separate `eql_v3_internal`
|
|
550
|
+
schema (grant it roles, never expose it — only `eql_v3` goes in Supabase's
|
|
551
|
+
Exposed schemas), and envelopes are versioned `v: 3`. Envelope production
|
|
552
|
+
rides on `@cipherstash/protect-ffi` 0.27, which takes an `eqlVersion` so the
|
|
553
|
+
same client emits v2 or v3 payloads per schema.
|
|
554
|
+
|
|
555
|
+
**Adapter behaviour:**
|
|
556
|
+
|
|
557
|
+
- columns are stored in their native `eql_v3.*` domains (raw jsonb payloads,
|
|
558
|
+
no composite wrap), with JS property → DB column name resolution and `Date`
|
|
559
|
+
reconstruction from `cast_as` on decrypted rows;
|
|
560
|
+
- **INTERIM:** filter operands are full storage envelopes — every `eql_v3.*`
|
|
561
|
+
domain CHECK requires the storage keys, and the SQL operators coerce their
|
|
562
|
+
operand into the domain, so a term-only operand is rejected today. This is
|
|
563
|
+
a tracked workaround (Linear CIP-3402), not the design: a full-envelope
|
|
564
|
+
operand carries a real decryptable ciphertext plus all of the column's
|
|
565
|
+
index terms, and PostgREST filters travel in GET query strings, so operands
|
|
566
|
+
can land in URL logs, proxies, and Supabase request logs (query terms are
|
|
567
|
+
index-terms-only by design). The fix is an EQL-side term-only scalar query
|
|
568
|
+
envelope (the scalar analog of `eql_v3.jsonb_query`);
|
|
569
|
+
- `like`/`ilike` on encrypted columns are emitted as PostgREST `cs`
|
|
570
|
+
(bloom-filter `@>`) — the v3 domains define no LIKE operator. Substring
|
|
571
|
+
search currently also requires `include_original: false` on the match
|
|
572
|
+
index; that requirement is a symptom of the same interim full-envelope
|
|
573
|
+
operand and goes away with CIP-3402;
|
|
574
|
+
- filters on storage-only columns (e.g. `types.Boolean`) and null filter
|
|
575
|
+
values are rejected at the type level and at runtime.
|
|
576
|
+
|
|
577
|
+
The v3 builder's default row type is exactly the table's inferred plaintext
|
|
578
|
+
shape (no index-signature widening — widening would disable the storage-only
|
|
579
|
+
filter guard). Filtering or inserting plaintext passthrough columns requires
|
|
580
|
+
an explicit row type: `es.from<typeof users, UserRow>('users', users)`.
|
|
581
|
+
|
|
582
|
+
The CLI gains an EQL v3 path: `stash eql install --eql-version 3` installs the
|
|
583
|
+
vendored `eql-3.0.0-alpha.2` bundle (`--supabase` selects the opclass-stripped
|
|
584
|
+
variant and applies the role grants for both `eql_v3` and `eql_v3_internal`);
|
|
585
|
+
`stash db upgrade` also accepts `--eql-version`, and `stash db status` reports
|
|
586
|
+
v2 and v3 installs independently. The v2 `SUPABASE_PERMISSIONS_SQL` block is
|
|
587
|
+
now generated from a shared `supabasePermissionsSql(schemaName)` helper, with
|
|
588
|
+
`SUPABASE_PERMISSIONS_SQL_V3` covering the v3 schemas.
|
|
589
|
+
|
|
590
|
+
- 5e4f354: Add the EQL v3 `text_search` authoring DSL on a new `@cipherstash/stack/eql/v3`
|
|
591
|
+
subpath (`types.TextSearch`, v3 `encryptedTable` / `buildEncryptConfig`). The v3
|
|
592
|
+
builders emit the existing `EncryptConfig` shape, so encryption, payloads, and
|
|
593
|
+
query paths are unchanged at runtime.
|
|
594
|
+
|
|
595
|
+
Also widens the public client types (`EncryptionClientConfig.schemas`,
|
|
596
|
+
`EncryptOptions`, `SearchTerm`/`EncryptQueryOptions`) to a structural contract so
|
|
597
|
+
both v2 and v3 builders are accepted by `Encryption` / `encrypt` / `decrypt` /
|
|
598
|
+
`encryptQuery`. This is a backward-compatible widening — existing v2 usage is
|
|
599
|
+
unaffected. The structural contracts themselves (`BuildableColumn`,
|
|
600
|
+
`BuildableQueryColumn`, `BuildableV3QueryableColumn`, `BuildableTable`,
|
|
601
|
+
`BuildableTableColumns`) and the `encryptModel` return-type mapper
|
|
602
|
+
(`EncryptedFromBuildableTable`) are exported from `@cipherstash/stack/types` so
|
|
603
|
+
consumers can name them.
|
|
604
|
+
|
|
605
|
+
- 4ceefed: Add a strongly-typed EQL v3 client surface on a new `@cipherstash/stack/v3`
|
|
606
|
+
subpath (`EncryptionV3`, `typedClient`, `TypedEncryptionClient`). It re-exports
|
|
607
|
+
the v3 `types` namespace and table API (from `@cipherstash/stack/eql/v3`), so a
|
|
608
|
+
single import provides everything needed to author and use a v3 schema.
|
|
609
|
+
|
|
610
|
+
Every method derives its types from the concrete `table` / `column` builder
|
|
611
|
+
arguments:
|
|
612
|
+
|
|
613
|
+
- `encrypt` / `encryptQuery` pin the plaintext to the column's domain type
|
|
614
|
+
(`text → string`, `int8 → bigint`, `timestamptz → Date`, …).
|
|
615
|
+
- `encryptQuery` constrains `queryType` to the column's capabilities and rejects
|
|
616
|
+
storage-only columns at compile time.
|
|
617
|
+
- `encryptModel` / `bulkEncryptModels` validate schema-column fields against their
|
|
618
|
+
inferred plaintext type (passthrough fields are untouched) and return a precise
|
|
619
|
+
encrypted model.
|
|
620
|
+
- `decryptModel` / `bulkDecryptModels` return the precise plaintext model,
|
|
621
|
+
reconstructing `Date` / `bigint` values from the encrypt-config `cast_as`.
|
|
622
|
+
|
|
623
|
+
Because the typed methods bind to the concrete branded v3 classes, a hand-rolled
|
|
624
|
+
structural table/column is rejected — closing the soundness gap where a non-branded
|
|
625
|
+
table could be encrypted at runtime while typed as plaintext.
|
|
626
|
+
|
|
627
|
+
Runtime behaviour is unchanged: the encrypt/query paths return the same operations
|
|
628
|
+
as the base client; only the model-decrypt paths add a per-column `Date` / `bigint`
|
|
629
|
+
reconstruction step. The v2 client surface (`Encryption`) is untouched.
|
|
630
|
+
|
|
631
|
+
- cb34d71: Add EQL v3 schema builders for all generated SQL domains under `@cipherstash/stack/eql/v3`, exposed as the `types` namespace (one member per EQL v3 domain, e.g. `types.TextEq` / `types.Int4Ord` / `types.Timestamptz`), including explicit query capability metadata (`getQueryCapabilities()` / `isQueryable()`) and v3 table support in model encryption helpers (`encryptModel` / `bulkEncryptModels`).
|
|
632
|
+
|
|
633
|
+
Also widen the accepted plaintext input type for `encrypt` / `encryptQuery` to include `Date` and `bigint` (via the new `Plaintext` type), so v3 `date` / `timestamptz` / `int8` domains can be encrypted and queried with their natural JavaScript values.
|
|
634
|
+
|
|
635
|
+
- 90d19fb: Rename the encryption client's auth strategy config field from `config.strategy` to **`config.authStrategy`** to make its purpose clear, and expand the `Encryption()` TypeDoc with a full authentication and keysets walkthrough.
|
|
636
|
+
|
|
637
|
+
**`config.authStrategy`** is the new, documented field for supplying an auth strategy (`OidcFederationStrategy`, `AccessKeyStrategy`, or any `{ getToken() }` object). **`config.strategy` is retained as a deprecated alias** — passing it still works and forwards to the client, but logs a one-time runtime deprecation warning. When both are set, `authStrategy` wins (and the deprecation warning still fires so the leftover field gets cleaned up).
|
|
638
|
+
|
|
639
|
+
```ts
|
|
640
|
+
import { Encryption, OidcFederationStrategy } from "@cipherstash/stack";
|
|
641
|
+
|
|
642
|
+
const client = await Encryption({
|
|
643
|
+
schemas: [users],
|
|
644
|
+
config: {
|
|
645
|
+
authStrategy: OidcFederationStrategy.create(workspaceCrn, () =>
|
|
646
|
+
getUserJwt()
|
|
647
|
+
),
|
|
648
|
+
},
|
|
649
|
+
});
|
|
650
|
+
```
|
|
651
|
+
|
|
652
|
+
**Migration:** rename `config.strategy` → `config.authStrategy`. No behavioural change beyond the deprecation warning; the field is forwarded to protect-ffi's `strategy` option exactly as before.
|
|
653
|
+
|
|
654
|
+
The `Encryption()` TypeDoc now documents the default `auto` strategy (env vars → local dev profile via `npx stash auth login`), the four `CS_*` production/CI variables, custom strategies (`AccessKeyStrategy`, `OidcFederationStrategy`), lock context, and keysets for multi-tenant isolation.
|
|
655
|
+
|
|
656
|
+
The `@cipherstash/stack/wasm-inline` entry (Deno / Edge / Workers / Bun) gets the same rename so the Node and WASM interfaces stay in sync: `WasmClientConfig.authStrategy` is the documented field, `strategy` is a deprecated alias that still works and warns at runtime.
|
|
657
|
+
|
|
658
|
+
- a5f5422: Bump `@cipherstash/auth` (and its per-platform native bindings) from `0.40.0` to `0.41.0`, and migrate to its new `Result`-returning API.
|
|
659
|
+
|
|
660
|
+
**What changed in `@cipherstash/auth` `0.41`.** Every fallible auth operation now returns a `@byteslice/result` `Result<T, AuthFailure>` (`{ data }` on success, `{ failure }` on error) instead of throwing. This covers strategy construction (`AccessKeyStrategy.create`, `OidcFederationStrategy.create`, `AutoStrategy.detect`, `DeviceSessionStrategy.fromProfile`), `getToken()`, and the device-code flow (`beginDeviceCodeFlow`, `pollForToken`, `openInBrowser`, `bindClientDevice`). Consumers now write `if (result.failure) …` and read `result.data` rather than `try/catch`. The `AuthError` type was renamed to **`AuthFailure`** — a discriminated union keyed by `type` (`"NOT_AUTHENTICATED"`, `"WORKSPACE_MISMATCH"`, …), replacing the old `error.code` string.
|
|
661
|
+
|
|
662
|
+
**`@cipherstash/stack` (breaking type surface).**
|
|
663
|
+
|
|
664
|
+
- **`AuthError` is renamed to `AuthFailure`** in the public re-exports from `@cipherstash/stack`. `AuthErrorCode` and `TokenResult` are unchanged. Anyone importing `AuthError` from `@cipherstash/stack` must switch to `AuthFailure`.
|
|
665
|
+
- The WASM-inline access-key path (`resolveStrategy`, used by `@cipherstash/stack/wasm-inline`'s `Encryption()`) now unwraps the `Result` from `AccessKeyStrategy.create`. A construction failure (e.g. an invalid CRN or access key) throws a descriptive `[encryption]` error naming the `AuthFailure.type` instead of surfacing the raw auth error.
|
|
666
|
+
- Bump `@cipherstash/protect-ffi` from `0.27.0` to `0.28.0`. auth `0.41`'s `getToken()` returns the token inside a `Result` envelope; protect-ffi `0.28` unwraps it (`.data.token`) inside its WASM `newClient`, whereas `0.27` read `.token` off the envelope and got `undefined` — which failed the WASM encrypt/decrypt round-trip with `token field is not a string`. `0.28` is the floor for the WASM path under auth `0.41`.
|
|
667
|
+
|
|
668
|
+
**`stash` (CLI) and `@cipherstash/wizard`.** Internal auth call sites (`stash auth login`, device binding, `init` auth check, and the wizard's token acquisition / prerequisite check) were updated to unwrap `Result` and branch on `failure.type`. Behaviour is preserved — auth failures still surface the same way to end users; no CLI/wizard API changed.
|
|
669
|
+
|
|
670
|
+
- 35b9ed6: Bump `@cipherstash/protect-ffi` to `0.26.0` and `@cipherstash/auth` to `0.40.0`, and replace the lock-context token ceremony with a strategy-based approach for identity-bound encryption.
|
|
671
|
+
|
|
672
|
+
**protect-ffi `0.26.0`** supersedes `0.25.0`. The public API is unchanged from `0.25` (internal fixes only). As in `0.25`, `serviceToken` is gone from the encrypt / decrypt / query option types; auth flows through the client's strategy / credentials, and lock contexts travel as `lockContext.identityClaim`. The WASM-inline path takes a single options object with the auth strategy nested under `strategy`, and `Encryption()` config uses **`workspaceCrn`** (`CS_WORKSPACE_CRN`) as the single source of truth — `CS_REGION` is no longer consulted. On that path `workspaceCrn` is required only alongside an `accessKey` (it derives the region); with a pre-built `strategy` it is **optional**, since the strategy already carries the CRN.
|
|
673
|
+
|
|
674
|
+
**Strategy-based, identity-bound encryption.** `OidcFederationStrategy` federates an end user's third-party OIDC JWT (Clerk, Supabase, Auth0, …) into a CTS service token. As of `@cipherstash/auth` `0.40` it takes a `workspaceCrn` (region derived from the CRN), matching `AccessKeyStrategy`. Pass it as `config.strategy` so every ZeroKMS request authenticates _as that user_, then bind the data key to a claim with `.withLockContext({ identityClaim })`:
|
|
675
|
+
|
|
676
|
+
```ts
|
|
677
|
+
import { Encryption, OidcFederationStrategy } from "@cipherstash/stack";
|
|
678
|
+
|
|
679
|
+
const client = await Encryption({
|
|
680
|
+
schemas: [users],
|
|
681
|
+
config: {
|
|
682
|
+
strategy: OidcFederationStrategy.create(workspaceCrn, () => getUserJwt()),
|
|
683
|
+
},
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
await client
|
|
687
|
+
.encrypt("alice@example.com", { column: users.email, table: users })
|
|
688
|
+
.withLockContext({ identityClaim: ["sub"] });
|
|
689
|
+
```
|
|
690
|
+
|
|
691
|
+
This replaces the old ceremony (`new LockContext()` → `await lc.identify(jwt)` → `.withLockContext(lc)`), which relied on a per-operation CTS token that protect-ffi removed in `0.25`.
|
|
692
|
+
|
|
693
|
+
- **`.withLockContext()`** now accepts a plain `{ identityClaim }` object (as well as a `LockContext`) and no longer requires a CTS token or an `identify()` call — it carries the identity claim only.
|
|
694
|
+
- **`LockContext.identify()` / `getLockContext()`** are **deprecated** (kept for backwards compatibility); the strategy handles token acquisition.
|
|
695
|
+
- **Strategies are re-exported** from `@cipherstash/stack` (`OidcFederationStrategy`, `AccessKeyStrategy`, `AutoStrategy`, `DeviceSessionStrategy`) and from `@cipherstash/stack/wasm-inline` (`OidcFederationStrategy`, `AccessKeyStrategy`) so integrators don't need a separate `@cipherstash/auth` install. `AuthStrategy` remains re-exported for the structural type.
|
|
696
|
+
|
|
697
|
+
**Migrating `region` → `workspaceCrn` (WASM-inline).** If you previously passed `region` (or relied on `CS_REGION`) to the WASM-inline `Encryption()` path, replace it with your workspace CRN: set `workspaceCrn` in config (or `CS_WORKSPACE_CRN` in the environment) to the value shown in the CipherStash dashboard (`crn:<region>.aws:<workspace-id>` — it embeds the region, which is now derived from it). `region` is ignored if passed.
|
|
698
|
+
|
|
699
|
+
**Lock-context enforcement is now server-side only.** Because the client no longer resolves a per-user CTS token at `withLockContext` time, it also cannot fail fast there: a wrong or missing identity claim surfaces as a ZeroKMS **decryption failure** (the data key simply doesn't unlock), not as a client-side error before the request. The cryptographic guarantee is unchanged — enforcement happens in ZeroKMS — but anyone relying on the old client-side throw for early feedback should assert on the operation's `failure` result instead.
|
|
700
|
+
|
|
701
|
+
Existing credential / env behaviour is preserved when `config.strategy` is omitted.
|
|
702
|
+
|
|
703
|
+
### Patch Changes
|
|
704
|
+
|
|
705
|
+
- aa9c4b1: Documentation: refresh package READMEs after the protectjs → stack repository rename. Fixed repository and license links, replaced dead in-repo docs links with cipherstash.com/docs URLs, rewrote the incorrect @cipherstash/nextjs README, and added guidance pointing new projects to @cipherstash/stack.
|
|
706
|
+
|
|
3
707
|
## 0.18.0
|
|
4
708
|
|
|
5
709
|
### Minor Changes
|