|
| 1 | +--- |
| 2 | +'@cipherstash/stack': minor |
| 3 | +--- |
| 4 | + |
| 5 | +Add `encryptedSupabaseV3` — the EQL v3 dialect of the Supabase adapter. It is |
| 6 | +now a connect-time-async factory: `await encryptedSupabaseV3(url, key)` (or |
| 7 | +`(client)`) introspects the database over `DATABASE_URL`, detects EQL v3 columns |
| 8 | +by their Postgres domain (`information_schema.columns.domain_name`), and derives |
| 9 | +each column's encryption config from its domain — callers no longer pass a |
| 10 | +schema to `from()`. `select('*')` is supported (expanded from the introspected |
| 11 | +column list, and aliased back to each declared column's JS property name so a |
| 12 | +property→DB rename round-trips). A column using an EQL v3 domain this SDK version does not model |
| 13 | +(e.g. `public.json`, `*_ord_ope`) throws at construction rather than silently |
| 14 | +passing through. Supplying `schemas` remains optional and adds compile-time |
| 15 | +types plus startup verification of the declared tables against the database. |
| 16 | +Requires a Postgres connection for introspection (`pg` is a new optional peer), |
| 17 | +so it cannot run in a Worker or the browser. |
| 18 | + |
| 19 | +Every column name a query carries — filters, `match`, `not`, raw `filter`, |
| 20 | +`or()`, `order()`, and the `onConflict` option — is now resolved from its JS |
| 21 | +property name to its DB column name in a single pass before the query is built, |
| 22 | +so a declared rename round-trips everywhere rather than only on the paths that |
| 23 | +remembered to translate. |
| 24 | + |
| 25 | +`order()` on ANY encrypted v3 column is now rejected — at compile time when |
| 26 | +`schemas` is supplied, and at runtime otherwise. The EQL v3 domains are |
| 27 | +`DOMAIN … AS jsonb` and the bundle declares no btree operator class on them, so |
| 28 | +`ORDER BY col` resolves through jsonb's default `jsonb_cmp` and sorts by the |
| 29 | +envelope's byte structure: a stable, plausible-looking, meaningless row order, |
| 30 | +with no error. Correct ordering is `ORDER BY eql_v3.ord_term(col)`, which |
| 31 | +PostgREST's `order=` cannot express. Order by a plaintext column, expose |
| 32 | +`eql_v3.ord_term()` as a generated column or view, or use the EQL v3 Drizzle |
| 33 | +integration, which emits `ord_term` directly. Note `gte`/`lte` filters remain |
| 34 | +correct: the comparison operators *are* declared on the ord domains, and only |
| 35 | +sorting resolves through the missing operator class. |
| 36 | + |
| 37 | +`.or()` now understands PostgREST's `column.not.<op>.<value>` negation. It was |
| 38 | +previously parsed as `{ op: 'not', value: '<op>.<value>' }`, so on an encrypted |
| 39 | +column `or('nickname.not.in.(ada,grace)')` encrypted the literal string |
| 40 | +`in.(ada,grace)` as a single plaintext and produced a filter that silently |
| 41 | +matched nothing. |
| 42 | + |
| 43 | +Free-text search on the v3 builder is `contains(column, value)`. `like`/`ilike` |
| 44 | +are not exposed, because EQL v3 free-text search is token containment over a |
| 45 | +bloom filter (`@>`, backed by `eql_v3.contains`) rather than SQL wildcard |
| 46 | +matching — `%` is tokenized like any other character, so a `like` pattern is a |
| 47 | +category error. This matches the v3 Drizzle integration, which omits them for |
| 48 | +the same reason. On an encrypted column `like`/`ilike` now throw and name |
| 49 | +`contains`; on a plaintext column they remain ordinary PostgREST filters. |
| 50 | + |
| 51 | +`contains` is narrowed at compile time to columns whose domain carries the |
| 52 | +`freeTextSearch` capability (`public.text_match`, `public.text_search`), and |
| 53 | +guarded at runtime for the untyped surface. A raw `filter(column, operator, …)` |
| 54 | +on an encrypted v3 column now derives its query type from the operator instead |
| 55 | +of always encrypting an equality term, so `filter('bio', 'cs', …)` on a |
| 56 | +`public.text_match` column works rather than being rejected, and an unsupported |
| 57 | +operator throws instead of silently encrypting the wrong term. |
| 58 | + |
| 59 | +Substring `contains` still matches only when the needle equals the stored value |
| 60 | +or is exactly the tokenizer's window (3 characters): the operand is a storage |
| 61 | +envelope whose bloom carries the whole needle as an `include_original` token. |
| 62 | +This is shared with v3 Drizzle's `contains` and tracked upstream in EQL. |
| 63 | + |
| 64 | +v2 (`encryptedSupabase`) is unchanged: it keeps `like`/`ilike` (`eql_v2.like`, |
| 65 | +`~~`) and its raw-`filter` query-type mapping, so no v2 ciphertext moves. |
0 commit comments