Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
67 commits
Select commit Hold shift + click to select a range
a3f8fcf
docs(stack): design spec for eql_v3 text_search schema DSL (increment 1)
tobyhede Jun 30, 2026
2808524
docs(stack): implementation plan for eql_v3 text_search schema DSL
tobyhede Jun 30, 2026
047163b
docs(stack): incorporate plan review feedback for eql_v3 text_search
tobyhede Jun 30, 2026
e4bb5e3
docs(stack): scope v3 to working client integration + apply batch-2 r…
tobyhede Jun 30, 2026
adb3ca9
docs(stack): apply batch-3 plan review (widen internal consumers, sco…
tobyhede Jun 30, 2026
04a0681
docs(stack): split query column contract so encryptQuery rejects non-…
tobyhede Jun 30, 2026
af41a95
docs(stack): pin bulk-encrypt widen-site + note WASM v3 boundary
tobyhede Jun 30, 2026
fd6ddb2
docs(stack): pin concrete BuiltMatchIndexOpts type for v3 match opts
tobyhede Jun 30, 2026
ed32da3
docs(stack): fix @ts-expect-error placement in negative type tests
tobyhede Jun 30, 2026
6566fee
feat(stack): add eql_v3 text_search column builder
tobyhede Jun 30, 2026
1c72fa2
feat(stack): add eql_v3 encryptedTable and buildEncryptConfig
tobyhede Jun 30, 2026
adacd24
feat(stack): wire @cipherstash/stack/schema/v3 export subpath
tobyhede Jun 30, 2026
482afe4
test(stack): type-level tests for eql_v3 schema DSL + scoped CI typec…
tobyhede Jun 30, 2026
ca11c2b
feat(stack): widen public client types so v3 builders work with the c…
tobyhede Jun 30, 2026
5e4f354
chore(stack): changeset for eql_v3 text_search DSL (minor)
tobyhede Jun 30, 2026
14492d9
style(stack): biome format v3 tests + drop now-unused EncryptedTable …
tobyhede Jun 30, 2026
5a633c4
fix(stack): guard v3 encryptedTable against reserved column names
tobyhede Jun 30, 2026
21ee03c
fix(stack): resolve column name structurally in wasm-inline encrypt e…
tobyhede Jun 30, 2026
eb3d7de
feat(stack): guard v3 buildEncryptConfig against duplicate table names
tobyhede Jun 30, 2026
ccfe6d9
feat(stack): extend v3 reserved-column guard to inherited prototype keys
tobyhede Jun 30, 2026
f956d7e
docs(stack): note text_search defaults to equality, needs queryType:'…
tobyhede Jun 30, 2026
3ce855e
test(stack): cover eql v3 typed schema domains
tobyhede Jul 1, 2026
5a64f04
feat(stack): add eql v3 domain builders for all generated SQL domains
tobyhede Jul 1, 2026
2103e43
feat(stack): support v3 tables and Date/bigint in client encryption
tobyhede Jul 1, 2026
a3cf310
test(stack): stabilize v3 client, pg, and wasm-inline coverage
tobyhede Jul 1, 2026
cb34d71
chore(stack): add changeset for eql v3 typed schema
tobyhede Jul 1, 2026
5c9771e
docs: add eql v3 typed schema plan and query API walkthrough
tobyhede Jul 1, 2026
20f5b94
fix(stack): address PR review feedback for eql v3 typed schema
tobyhede Jul 1, 2026
4ceefed
feat(stack): strongly-typed EQL v3 client surface (@cipherstash/stack…
tobyhede Jul 1, 2026
298dfc6
fix(stack): use string plaintext for eql v3 int8 domains
tobyhede Jul 1, 2026
34d5d01
fix(stack): address code review findings for eql v3 typed client
tobyhede Jul 1, 2026
b6496df
fix(stack): key v3 encrypt config by DB column name, not JS property
tobyhede Jul 1, 2026
5fc710f
fix(stack): match v3 model fields by JS property, encrypt by DB name
tobyhede Jul 1, 2026
d41fa99
ci(stack): add blocking FTA complexity gate for EQL v3
tobyhede Jul 1, 2026
1cde5e3
test(stack): type-driven v3 domain test matrix
tobyhede Jul 2, 2026
d0fdf90
feat(stack): equality-via-ORE fix + live v3 domain coverage
tobyhede Jul 2, 2026
a50e512
test(stack): live Postgres SQL coverage for all 35 v3 domains
tobyhede Jul 2, 2026
67dffc3
fix(stack): address CodeRabbit review findings on eql v3 typed client
tobyhede Jul 2, 2026
ea1c1c7
test(stack): harden v3 CJS export check and preserve run() transcript…
tobyhede Jul 2, 2026
da75be9
test(stack): disable timestamptz and matrix-live-pg tests failing on CI
tobyhede Jul 2, 2026
0b6fafe
fix(stack): wrap ciphertext params with sql.json() in matrix-live-pg
tobyhede Jul 2, 2026
0e60bd8
docs(stack): design for Stryker v3 blocking CI gate
tobyhede Jul 2, 2026
f032ee3
docs(stack): confirm lean, single-workflow Stryker v3 gate
tobyhede Jul 2, 2026
6bcd670
refactor(stack): extract EQL v3 DSL into eql/v3 types module
tobyhede Jul 3, 2026
92d3e8e
fix(stack): guard against duplicate v3 column DB names in build()
tobyhede Jul 3, 2026
a03d2cc
fix(stack): export v3 Buildable* contracts from public types entrypoint
tobyhede Jul 3, 2026
0f4b37e
test(stack): migrate v3-matrix suite to eql/v3 types namespace
tobyhede Jul 3, 2026
748b949
test(stack): cover lock-context encrypt guards and wasm newClient shape
tobyhede Jul 3, 2026
fce4619
test(stack): re-enable matrix-live-pg live SQL coverage (stale skip)
tobyhede Jul 3, 2026
476c1df
fix(stack): emit hm index for text order domains (eql_v3 SQL domain r…
tobyhede Jul 3, 2026
17b0971
test(stack): v3 lock-context coverage + text_match/no-op/empty-config…
tobyhede Jul 3, 2026
b44eb55
chore(stack): EQL v3 maintainability follow-ups from the #547 review
freshtonic Jul 4, 2026
ecd3f38
feat(stack): encryptedSupabaseV3 — EQL v3 dialect of the Supabase ada…
freshtonic Jul 3, 2026
d15414a
feat(cli): EQL v3 install path — vendored bundles + --eql-version flag
freshtonic Jul 3, 2026
542095d
test(stack): live Supabase suites for v3 + the missing v2 range baseline
freshtonic Jul 3, 2026
cc62407
docs(stack): EQL v3 Supabase docs + changeset
freshtonic Jul 3, 2026
4a7d060
fix(stack,cli): address #547 review — operand/typing guards, v3-aware…
freshtonic Jul 4, 2026
b9a5f20
chore: retrigger CI after rebase onto main
freshtonic Jul 4, 2026
36a9b45
fix(stack): matrix-live-pg — ob-carrying text domains cannot store ''
freshtonic Jul 4, 2026
6374804
fix(stack): matrix-live-pg storage proof — parse lone-decrypt output …
freshtonic Jul 4, 2026
21f8f3e
docs(stack,cli): follow the stash db install → stash eql install rename
freshtonic Jul 4, 2026
a580c48
chore(stack,cli): re-vendor EQL v3 bundle from upstream eql-3.0.0-alp…
freshtonic Jul 4, 2026
d28070b
docs(stack,cli): re-baseline EQL v3 Supabase docs on eql-3.0.0-alpha.2
freshtonic Jul 4, 2026
e626025
refactor(stack)!: rename EQL v3 scalar domains to SQL-standard names …
freshtonic Jul 4, 2026
ba91f7c
feat(stack): adopt protect-ffi 0.27 and wire eqlVersion through the c…
freshtonic Jul 4, 2026
fe80490
fix(stack,cli): integrate the re-baseline — v3 wire in live suites, h…
freshtonic Jul 4, 2026
f3e4c3e
test(stack): bulkDecrypt malformed ciphertexts are per-item errors un…
freshtonic Jul 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions .changeset/eql-v3-supabase.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
---
'@cipherstash/stack': minor
'stash': minor
---

Add EQL v3 Supabase support, baselined on the `eql-3.0.0-alpha.2` release.

`@cipherstash/stack/supabase` gains `encryptedSupabaseV3` — the EQL v3
counterpart of `encryptedSupabase` for schemas authored with
`@cipherstash/stack/eql/v3`. The public surface and call shape are identical
to v2 (same filter methods, `withLockContext`, `audit`); only the schema type
and wire encoding differ.

**The v3 surface** is the `eql-3.0.0-alpha.2` release artifact: domains use
SQL-standard type names (`eql_v3.integer_ord`, `eql_v3.timestamp_ord`,
`eql_v3.boolean`, … mirrored by `types.IntegerOrd`, `types.TimestampOrd`,
`types.Boolean`, …), SEM internals live in a separate `eql_v3_internal`
schema (grant it roles, never expose it — only `eql_v3` goes in Supabase's
Exposed schemas), and envelopes are versioned `v: 3`. Envelope production
rides on `@cipherstash/protect-ffi` 0.27, which takes an `eqlVersion` so the
same client emits v2 or v3 payloads per schema.

**Adapter behaviour:**

- columns are stored in their native `eql_v3.*` domains (raw jsonb payloads,
no composite wrap), with JS property → DB column name resolution and `Date`
reconstruction from `cast_as` on decrypted rows;
- **INTERIM:** filter operands are full storage envelopes — every `eql_v3.*`
domain CHECK requires the storage keys, and the SQL operators coerce their
operand into the domain, so a term-only operand is rejected today. This is
a tracked workaround (Linear CIP-3402), not the design: a full-envelope
operand carries a real decryptable ciphertext plus all of the column's
index terms, and PostgREST filters travel in GET query strings, so operands
can land in URL logs, proxies, and Supabase request logs (query terms are
index-terms-only by design). The fix is an EQL-side term-only scalar query
envelope (the scalar analog of `eql_v3.jsonb_query`);
- `like`/`ilike` on encrypted columns are emitted as PostgREST `cs`
(bloom-filter `@>`) — the v3 domains define no LIKE operator. Substring
search currently also requires `include_original: false` on the match
index; that requirement is a symptom of the same interim full-envelope
operand and goes away with CIP-3402;
- filters on storage-only columns (e.g. `types.Boolean`) and null filter
values are rejected at the type level and at runtime.

The v3 builder's default row type is exactly the table's inferred plaintext
shape (no index-signature widening — widening would disable the storage-only
filter guard). Filtering or inserting plaintext passthrough columns requires
an explicit row type: `es.from<typeof users, UserRow>('users', users)`.

The CLI gains an EQL v3 path: `stash eql install --eql-version 3` installs the
vendored `eql-3.0.0-alpha.2` bundle (`--supabase` selects the opclass-stripped
variant and applies the role grants for both `eql_v3` and `eql_v3_internal`);
`stash db upgrade` also accepts `--eql-version`, and `stash db status` reports
v2 and v3 installs independently. The v2 `SUPABASE_PERMISSIONS_SQL` block is
now generated from a shared `supabasePermissionsSql(schemaName)` helper, with
`SUPABASE_PERMISSIONS_SQL_V3` covering the v3 schemas.
18 changes: 18 additions & 0 deletions .changeset/eql-v3-text-search.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
"@cipherstash/stack": minor
---

Add the EQL v3 `text_search` authoring DSL on a new `@cipherstash/stack/eql/v3`
subpath (`types.TextSearch`, v3 `encryptedTable` / `buildEncryptConfig`). The v3
builders emit the existing `EncryptConfig` shape, so encryption, payloads, and
query paths are unchanged at runtime.

Also widens the public client types (`EncryptionClientConfig.schemas`,
`EncryptOptions`, `SearchTerm`/`EncryptQueryOptions`) to a structural contract so
both v2 and v3 builders are accepted by `Encryption` / `encrypt` / `decrypt` /
`encryptQuery`. This is a backward-compatible widening — existing v2 usage is
unaffected. The structural contracts themselves (`BuildableColumn`,
`BuildableQueryColumn`, `BuildableV3QueryableColumn`, `BuildableTable`,
`BuildableTableColumns`) and the `encryptModel` return-type mapper
(`EncryptedFromBuildableTable`) are exported from `@cipherstash/stack/types` so
consumers can name them.
29 changes: 29 additions & 0 deletions .changeset/eql-v3-typed-client.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
"@cipherstash/stack": minor
---

Add a strongly-typed EQL v3 client surface on a new `@cipherstash/stack/v3`
subpath (`EncryptionV3`, `typedClient`, `TypedEncryptionClient`). It re-exports
the v3 `types` namespace and table API (from `@cipherstash/stack/eql/v3`), so a
single import provides everything needed to author and use a v3 schema.

Every method derives its types from the concrete `table` / `column` builder
arguments:

- `encrypt` / `encryptQuery` pin the plaintext to the column's domain type
(`text → string`, `int8 → bigint`, `timestamptz → Date`, …).
- `encryptQuery` constrains `queryType` to the column's capabilities and rejects
storage-only columns at compile time.
- `encryptModel` / `bulkEncryptModels` validate schema-column fields against their
inferred plaintext type (passthrough fields are untouched) and return a precise
encrypted model.
- `decryptModel` / `bulkDecryptModels` return the precise plaintext model,
reconstructing `Date` / `bigint` values from the encrypt-config `cast_as`.

Because the typed methods bind to the concrete branded v3 classes, a hand-rolled
structural table/column is rejected — closing the soundness gap where a non-branded
table could be encrypted at runtime while typed as plaintext.

Runtime behaviour is unchanged: the encrypt/query paths return the same operations
as the base client; only the model-decrypt paths add a per-column `Date` / `bigint`
reconstruction step. The v2 client surface (`Encryption`) is untouched.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
7 changes: 7 additions & 0 deletions .changeset/eql-v3-typed-schema.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@cipherstash/stack': minor
---

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`).

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.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
60 changes: 60 additions & 0 deletions .github/workflows/fta-v3.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
name: "FTA Complexity (EQL v3)"

# Blocking complexity gate for the EQL v3 text-search schema. Runs the Fast
# TypeScript Analyzer (fta-cli) against the v3 source directory only and fails
# the check when any file exceeds the score cap (`--score-cap` in the
# `analyze:complexity` script). FTA is pure static source analysis, so this job
# needs no build step, database, or credentials.

on:
push:
branches:
- 'main'
paths:
- 'packages/stack/src/eql/v3/**'
- 'packages/stack/package.json'
- '.github/workflows/fta-v3.yml'
pull_request:
branches:
- "**"
paths:
- 'packages/stack/src/eql/v3/**'
- 'packages/stack/package.json'
- '.github/workflows/fta-v3.yml'

permissions:
contents: read

jobs:
fta:
name: Analyze v3 complexity
runs-on: blacksmith-4vcpu-ubuntu-2404

steps:
- name: Checkout Repo
uses: actions/checkout@v6

- uses: pnpm/action-setup@v6.0.8
name: Install pnpm
with:
run_install: false

- name: Install Node.js
uses: actions/setup-node@v6
with:
node-version: 22
cache: 'pnpm'

# node-pty's install hook falls back to `node-gyp rebuild` when no
# linux-x64 prebuild matches. pnpm/action-setup v6 no longer ships
# node-gyp on PATH, so install it explicitly.
- name: Install node-gyp
run: npm install -g node-gyp

- name: Install dependencies
run: pnpm install --frozen-lockfile

# Non-zero exit (score above the cap) fails the check — this is the
# blocking gate. No `continue-on-error`.
- name: Analyze v3 complexity
run: pnpm --filter @cipherstash/stack run analyze:complexity
13 changes: 12 additions & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,23 @@ jobs:
- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Type tests (stack)
run: pnpm --filter @cipherstash/stack run test:types

- name: Lint — no hardcoded package-manager runners
run: pnpm run lint:runners

- name: Test — lint script self-tests
run: pnpm run test:scripts

# The EQL v3 bundles in packages/cli/src/sql are vendored (derived from
# the stack fixture monolith by gen:eql-v3-sql). Regenerate and fail on
# any diff so the fixture and the shipped bundles cannot drift silently.
- name: Check — vendored EQL v3 SQL bundles are in sync
run: |
pnpm --filter stash run gen:eql-v3-sql
git diff --exit-code -- packages/cli/src/sql/cipherstash-encrypt-v3.sql packages/cli/src/sql/cipherstash-encrypt-v3-supabase.sql

- name: Create .env file in ./packages/protect/
run: |
touch ./packages/protect/.env
Expand Down Expand Up @@ -157,7 +168,7 @@ jobs:
run: pnpm exec turbo run test:e2e --filter @cipherstash/e2e

# Verifies @cipherstash/stack/wasm-inline works under Deno — i.e. the
# WASM build of protect-ffi 0.25+ and auth 0.38+ can round-trip an
# WASM build of protect-ffi 0.26+ and auth 0.40+ can round-trip an
# encryption against ZeroKMS / CTS in a runtime with no native
# bindings available. The deno.json deliberately omits --allow-ffi so
# a silent fallback to the NAPI module is impossible.
Expand Down
77 changes: 77 additions & 0 deletions docs/query-api-walkthrough.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# Query API Walkthrough — API → FFI → CipherStash Client

How a query value travels from the public API down to the Rust SDK across the FFI boundary. Terse by design.

## Flow

```mermaid
flowchart TD
subgraph JS["@cipherstash/stack (TypeScript)"]
A["User query builder<br/>ops.eq / Supabase filter / client.encryptQuery()"]
B["EncryptionClient.encryptQuery(value | terms[])<br/>encryption/index.ts:259"]
C["EncryptQueryOperation.execute()<br/>BatchEncryptQueryOperation.execute()"]
D["resolveIndexType() + queryTypeToFfi/QueryOp<br/>build QueryPayload{plaintext,column,table,indexType,queryOp}"]
E["validate: validateNumericValue<br/>assertValueIndexCompatibility"]
end

subgraph FFI["@cipherstash/protect-ffi (Neon bindings)"]
F["JS wrapper encryptQuery / encryptQueryBulk<br/>lib/index.cjs:155"]
G["native handle via @neon-rs/load<br/>lib/load.cjs:9"]
H["platform .node addon<br/>protect-ffi-darwin-arm64/index.node"]
end

subgraph RUST["CipherStash Client (Rust SDK)"]
I["EQL term generation<br/>ORE / match / unique / ste_vec"]
J["ZeroKMS key ops"]
end

A --> B --> C --> E --> D --> F --> G --> H --> I
I --> J
I -- "Encrypted | EncryptedQuery" --> F
F -- "formatEncryptedResult()" --> C
C -- "SQL/PostgREST WHERE clause" --> A
```

## Layers

| # | Layer | Entry point | Role |
|---|-------|-------------|------|
| 1 | Public API | `encryption/index.ts:259/270` `encryptQuery()` | Overloaded: single value → `EncryptQueryOperation`; `ScalarQueryTerm[]` → `BatchEncryptQueryOperation`. |
| 1a | Query builders | `drizzle/operators.ts:976`, `supabase/query-builder.ts:44` | `eq/gt/...` operators & deferred builders that batch-encrypt RHS values, then emit a WHERE clause. |
| 2 | Operations | `operations/encrypt-query.ts:41`, `operations/batch-encrypt-query.ts:115` | `execute()`: validate → resolve index → call FFI. `*WithLockContext` resolves `LockContextInput` via `resolveLockContext` before the FFI call. |
| 3 | EQL resolution | `helpers/infer-index-type.ts:89`, `types.ts:292` | `resolveIndexType` + `queryTypeToFfi`/`queryTypeToQueryOp` map public `QueryTypeName` → FFI `indexType`/`queryOp`. |
| 4 | FFI JS wrapper | `protect-ffi/lib/index.cjs:155` | `encryptQuery`/`encryptQueryBulk` → `wrapAsync(native.*)`. |
| 5 | Native loader | `protect-ffi/lib/load.cjs:9` | `@neon-rs/load` proxies to the platform prebuilt `.node`. |
| 6 | Rust SDK | compiled into `.node` | CipherStash Client: encryption, EQL/ORE/STE-vec term gen, ZeroKMS. Not a JS dep — shipped inside the addon. |

## Query-type mapping (Layer 3)

```mermaid
flowchart LR
subgraph Public["QueryTypeName"]
eq[equality]
ord[orderAndRange]
txt[freeTextSearch]
sel[steVecSelector]
trm[steVecTerm]
end
subgraph FFI["indexType / queryOp"]
u[unique]
o[ore]
m[match]
sv[ste_vec]
end
eq --> u
ord --> o
txt --> m
sel --> sv
trm --> sv
```

## Notes

- **Client init:** `EncryptionClient.init()` (`encryption/index.ts:81`) calls FFI `newClient()` once; the returned `Client` handle is passed into every `encryptQuery` call.
- **`cipherstashclient`** = the CipherStash Client **Rust SDK**, compiled via Neon into the platform `.node` binary inside `@cipherstash/protect-ffi`. It performs the actual crypto and talks to ZeroKMS.
- **Result shape:** `EncryptedQueryResult` (`types.ts:175`); shaped by `formatEncryptedResult(..., returnType)` (`eql` vs raw).
- **Version:** `package.json` pins `@cipherstash/protect-ffi@0.24.0` (installed tree observed at `0.23.0` — confirm before relying on it).
- `packages/protect/src/ffi/*` mirrors this flow under the older `protect` package name.
Loading
Loading