Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
a65856d
docs(stack): design spec for eql_v3 text_search schema DSL (increment 1)
tobyhede Jun 30, 2026
73c1ab3
docs(stack): implementation plan for eql_v3 text_search schema DSL
tobyhede Jun 30, 2026
71e0198
docs(stack): incorporate plan review feedback for eql_v3 text_search
tobyhede Jun 30, 2026
35bb46c
docs(stack): scope v3 to working client integration + apply batch-2 r…
tobyhede Jun 30, 2026
a822660
docs(stack): apply batch-3 plan review (widen internal consumers, sco…
tobyhede Jun 30, 2026
445be40
docs(stack): split query column contract so encryptQuery rejects non-…
tobyhede Jun 30, 2026
1713caf
docs(stack): pin bulk-encrypt widen-site + note WASM v3 boundary
tobyhede Jun 30, 2026
840dc66
docs(stack): pin concrete BuiltMatchIndexOpts type for v3 match opts
tobyhede Jun 30, 2026
d8d659e
docs(stack): fix @ts-expect-error placement in negative type tests
tobyhede Jun 30, 2026
2fac5c8
feat(stack): add eql_v3 text_search column builder
tobyhede Jun 30, 2026
aaa88c9
feat(stack): add eql_v3 encryptedTable and buildEncryptConfig
tobyhede Jun 30, 2026
888c170
feat(stack): wire @cipherstash/stack/schema/v3 export subpath
tobyhede Jun 30, 2026
7a32262
test(stack): type-level tests for eql_v3 schema DSL + scoped CI typec…
tobyhede Jun 30, 2026
8c221d5
feat(stack): widen public client types so v3 builders work with the c…
tobyhede Jun 30, 2026
1aa9a11
chore(stack): changeset for eql_v3 text_search DSL (minor)
tobyhede Jun 30, 2026
656841f
style(stack): biome format v3 tests + drop now-unused EncryptedTable …
tobyhede Jun 30, 2026
28b3cfc
fix(stack): guard v3 encryptedTable against reserved column names
tobyhede Jun 30, 2026
4fad589
fix(stack): resolve column name structurally in wasm-inline encrypt e…
tobyhede Jun 30, 2026
8721ff7
feat(stack): guard v3 buildEncryptConfig against duplicate table names
tobyhede Jun 30, 2026
4515807
feat(stack): extend v3 reserved-column guard to inherited prototype keys
tobyhede Jun 30, 2026
ece1b7e
docs(stack): note text_search defaults to equality, needs queryType:'…
tobyhede Jun 30, 2026
359dd55
test(stack): cover eql v3 typed schema domains
tobyhede Jul 1, 2026
cae3eb7
feat(stack): add eql v3 domain builders for all generated SQL domains
tobyhede Jul 1, 2026
7f646bb
feat(stack): support v3 tables and Date/bigint in client encryption
tobyhede Jul 1, 2026
282c462
test(stack): stabilize v3 client, pg, and wasm-inline coverage
tobyhede Jul 1, 2026
b8a3d20
chore(stack): add changeset for eql v3 typed schema
tobyhede Jul 1, 2026
db2840f
docs: add eql v3 typed schema plan and query API walkthrough
tobyhede Jul 1, 2026
d142d9e
fix(stack): address PR review feedback for eql v3 typed schema
tobyhede Jul 1, 2026
af2d04e
feat(stack): strongly-typed EQL v3 client surface (@cipherstash/stack…
tobyhede Jul 1, 2026
ba7f467
fix(stack): use string plaintext for eql v3 int8 domains
tobyhede Jul 1, 2026
430f487
fix(stack): address code review findings for eql v3 typed client
tobyhede Jul 1, 2026
b25bf4a
fix(stack): key v3 encrypt config by DB column name, not JS property
tobyhede Jul 1, 2026
ebf2c74
fix(stack): match v3 model fields by JS property, encrypt by DB name
tobyhede Jul 1, 2026
64a1def
ci(stack): add blocking FTA complexity gate for EQL v3
tobyhede Jul 1, 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
14 changes: 14 additions & 0 deletions .changeset/eql-v3-text-search.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
"@cipherstash/stack": minor
---

Add the EQL v3 `text_search` authoring DSL on a new `@cipherstash/stack/schema/v3`
subpath (`encryptedTextSearchColumn`, 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.
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 schema builders, 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.
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/schema/v3`, 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.
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/schema/v3/**'
- 'packages/stack/package.json'
- '.github/workflows/fta-v3.yml'
pull_request:
branches:
- "**"
paths:
- 'packages/stack/src/schema/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
3 changes: 3 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ 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

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