Skip to content

feat: support protect-ffi 0.30 JSON queries - #711

Merged
coderdan merged 9 commits into
mainfrom
codex/protect-ffi-0.30
Jul 21, 2026
Merged

coderdan merged 9 commits into
mainfrom
codex/protect-ffi-0.30

Conversation

@coderdan

@coderdan coderdan commented Jul 20, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • upgrade @cipherstash/protect-ffi to 0.30 and @cipherstash/eql to 3.0.2
  • add the new SteVec value-selector query operation and correct containment/ordering routing
  • add GIN-indexable JSON-path equality plus JSON-path ordering to Drizzle and Prisma Next
  • reject unsafe short free-text needles at the core FFI boundary
  • update the EQL v3 JSON storage/query domains, examples, public skills, and migration artifacts
  • detect EQL 3.0.2 in the Supabase adapter and fail closed for encrypted operators whose typed query-domain casts PostgREST cannot express

Upgrade notes

  • EQL 3.0.2 changes the SteVec storage format. Existing encrypted JSON rows must be re-encrypted before they can be queried.
  • Legacy EQL v2 searchableJson() schemas are rejected during client setup because protect-ffi 0.30 cannot emit the removed v2 selector envelope. Migrate these columns to EQL v3 types.Json.
  • With EQL 3.0.2, Supabase v3 fails fast for matches(), encrypted contains(), and selectorEq()/selectorNe(). PostgREST cannot express the required eql_v3.query_* casts; use Drizzle, Prisma Next, or a carefully scoped SQL/RPC path for those queries.

Validation

  • core targeted unit tests and type tests
  • Drizzle unit and type tests
  • Prisma Next unit tests, type-check, and build
  • Supabase unit and type tests, version-boundary tests, and build
  • all affected package builds
  • pnpm code:check
  • git diff --check

Closes #697
Closes #677

@coderdan
coderdan requested a review from a team as a code owner July 20, 2026 10:40
@coderabbitai

coderabbitai Bot commented Jul 20, 2026 •

Copy link
Copy Markdown
Contributor

Important

Review skipped

Too many files!

This PR contains 134 files, which is 34 over the limit of 100.

To get a review, narrow the scope:
• coderabbit review --committed # exclude uncommitted changes
• coderabbit review --dir # limit to a subdirectory
• coderabbit review --base # compare against a closer base

Upgrade to a paid plan to raise the limit.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 462e332f-ff86-41d7-b0fe-bdd67b40bbb5

📥 Commits

Reviewing files that changed from the base of the PR and between f0b20d3 and 434975a.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (137)
  • .changeset/protect-ffi-030-json-selectors.md
  • examples/prisma/README.md
  • examples/prisma/migrations/app/20260714T2142_initial/end-contract.d.ts
  • examples/prisma/migrations/app/20260714T2142_initial/end-contract.json
  • examples/prisma/migrations/app/20260714T2142_initial/migration.json
  • examples/prisma/migrations/app/20260714T2142_initial/migration.ts
  • examples/prisma/migrations/app/20260714T2142_initial/ops.json
  • examples/prisma/migrations/cipherstash/20260601T0100_install_eql_v3_bundle/migration.json
  • examples/prisma/migrations/cipherstash/20260601T0100_install_eql_v3_bundle/ops.json
  • examples/prisma/migrations/cipherstash/20260720T0000_upgrade_eql_v3_3_0_2/migration.json
  • examples/prisma/migrations/cipherstash/20260720T0000_upgrade_eql_v3_3_0_2/ops.json
  • examples/prisma/migrations/cipherstash/refs/head.json
  • examples/prisma/prisma/schema.prisma
  • examples/prisma/src/index.ts
  • examples/prisma/src/prisma/contract.d.ts
  • examples/prisma/src/prisma/contract.json
  • examples/prisma/test/e2e/README.md
  • examples/prisma/test/e2e/json.e2e.test.ts
  • examples/prisma/test/e2e/str-range.e2e.test.ts
  • packages/cli/package.json
  • packages/prisma-next/README.md
  • packages/prisma-next/integration/json-adapter.ts
  • packages/prisma-next/integration/json.integration.test.ts
  • packages/prisma-next/migrations/20260720T0000_upgrade_eql_v3_3_0_2/migration.json
  • packages/prisma-next/migrations/20260720T0000_upgrade_eql_v3_3_0_2/migration.ts
  • packages/prisma-next/migrations/20260720T0000_upgrade_eql_v3_3_0_2/ops.json
  • packages/prisma-next/migrations/refs/head.json
  • packages/prisma-next/package.json
  • packages/prisma-next/src/contract-authoring.ts
  • packages/prisma-next/src/exports/control.ts
  • packages/prisma-next/src/exports/runtime.ts
  • packages/prisma-next/src/exports/v3.ts
  • packages/prisma-next/src/extension-metadata/constants-v3.ts
  • packages/prisma-next/src/migration/eql-bundle-v3.ts
  • packages/prisma-next/src/types/codec-types.ts
  • packages/prisma-next/src/types/operation-types.ts
  • packages/prisma-next/src/v3/bulk-encrypt-v3.ts
  • packages/prisma-next/src/v3/operators-v3.ts
  • packages/prisma-next/src/v3/query-term.ts
  • packages/prisma-next/test/authoring.test.ts
  • packages/prisma-next/test/bundling-isolation.test.ts
  • packages/prisma-next/test/column-types.test.ts
  • packages/prisma-next/test/descriptor.test.ts
  • packages/prisma-next/test/live/helpers/harness.ts
  • packages/prisma-next/test/live/operators-live-pg.test.ts
  • packages/prisma-next/test/operation-types.types.test-d.ts
  • packages/prisma-next/test/psl-interpretation.test.ts
  • packages/prisma-next/test/v3/bulk-encrypt-v3.test.ts
  • packages/prisma-next/test/v3/catalog-invariants.test.ts
  • packages/prisma-next/test/v3/catalog.test.ts
  • packages/prisma-next/test/v3/codec-runtime-v3.test.ts
  • packages/prisma-next/test/v3/codec-types-v3.test-d.ts
  • packages/prisma-next/test/v3/constants-v3.test.ts
  • packages/prisma-next/test/v3/derive-schemas-v3.test.ts
  • packages/prisma-next/test/v3/migration-v3.test.ts
  • packages/prisma-next/test/v3/operator-gating-v3.test.ts
  • packages/prisma-next/test/v3/operator-lowering-v3-json.test.ts
  • packages/prisma-next/test/v3/operator-lowering-v3-text-search.test.ts
  • packages/prisma-next/test/v3/operator-lowering-v3.helpers.ts
  • packages/prisma-next/test/v3/properties.test.ts
  • packages/prisma-next/test/v3/runtime-v3.test.ts
  • packages/prisma-next/test/v3/sdk-adapter-v3.test.ts
  • packages/stack-drizzle/README.md
  • packages/stack-drizzle/__tests__/v3/operators.test.ts
  • packages/stack-drizzle/__tests__/v3/sql-dialect.test.ts
  • packages/stack-drizzle/integration/json-adapter.ts
  • packages/stack-drizzle/integration/json-contains.integration.test.ts
  • packages/stack-drizzle/integration/json-selector.integration.test.ts
  • packages/stack-drizzle/integration/json.integration.test.ts
  • packages/stack-drizzle/package.json
  • packages/stack-drizzle/src/v3/operators.ts
  • packages/stack-drizzle/src/v3/sql-dialect.ts
  • packages/stack-drizzle/tsconfig.json
  • packages/stack-supabase/README.md
  • packages/stack-supabase/__tests__/supabase-introspect.test.ts
  • packages/stack-supabase/__tests__/supabase-schema-builder.test.ts
  • packages/stack-supabase/__tests__/supabase-v3-builder.test.ts
  • packages/stack-supabase/__tests__/supabase-v3-factory.test.ts
  • packages/stack-supabase/__tests__/supabase-v3-json.test.ts
  • packages/stack-supabase/__tests__/supabase-v3-matrix.test.ts
  • packages/stack-supabase/integration/adapter.ts
  • packages/stack-supabase/integration/introspect.integration.test.ts
  • packages/stack-supabase/integration/json.integration.test.ts
  • packages/stack-supabase/integration/wire.integration.test.ts
  • packages/stack-supabase/package.json
  • packages/stack-supabase/src/index.ts
  • packages/stack-supabase/src/introspect.ts
  • packages/stack-supabase/src/query-builder-v3.ts
  • packages/stack-supabase/src/query-builder.ts
  • packages/stack-supabase/src/types.ts
  • packages/stack/README.md
  • packages/stack/__tests__/encrypt-query-match-preflight.test.ts
  • packages/stack/__tests__/encrypt-query-searchable-json.test.ts
  • packages/stack/__tests__/encrypt-query-stevec.test.ts
  • packages/stack/__tests__/eql-v3-domain-registry.test.ts
  • packages/stack/__tests__/error-codes.test.ts
  • packages/stack/__tests__/infer-index-type.test.ts
  • packages/stack/__tests__/init-strategy.test.ts
  • packages/stack/__tests__/match-bloom-live.test.ts
  • packages/stack/__tests__/searchable-json-pg.test.ts
  • packages/stack/__tests__/selector-path.test.ts
  • packages/stack/__tests__/test-kit-families.test.ts
  • packages/stack/__tests__/types.test-d.ts
  • packages/stack/__tests__/v3-matrix/matrix.test-d.ts
  • packages/stack/__tests__/wasm-inline-query.test.ts
  • packages/stack/integration/shared/json-crypto.integration.test.ts
  • packages/stack/integration/shared/match-bloom.integration.test.ts
  • packages/stack/integration/shared/matrix-crypto.integration.test.ts
  • packages/stack/integration/shared/matrix-sql.integration.test.ts
  • packages/stack/integration/shared/schema-pg.integration.test.ts
  • packages/stack/integration/wasm/adapter.ts
  • packages/stack/package.json
  • packages/stack/src/encryption/helpers/index.ts
  • packages/stack/src/encryption/helpers/infer-index-type.ts
  • packages/stack/src/encryption/helpers/validation.ts
  • packages/stack/src/encryption/index.ts
  • packages/stack/src/encryption/operations/batch-encrypt-query.ts
  • packages/stack/src/encryption/operations/encrypt-query.ts
  • packages/stack/src/eql/v3/columns.ts
  • packages/stack/src/eql/v3/selector-path.ts
  • packages/stack/src/schema/index.ts
  • packages/stack/src/schema/match-defaults.ts
  • packages/stack/src/types.ts
  • packages/stack/src/wasm-inline.ts
  • packages/test-kit/package.json
  • packages/test-kit/src/catalog.ts
  • packages/test-kit/src/families.ts
  • packages/test-kit/src/index.ts
  • packages/test-kit/src/json-adapter.ts
  • packages/test-kit/src/ops.ts
  • packages/test-kit/src/run-family-suite.ts
  • packages/test-kit/src/run-json-suite.ts
  • skills/stash-drizzle/SKILL.md
  • skills/stash-encryption/SKILL.md
  • skills/stash-prisma-next/SKILL.md
  • skills/stash-supabase/SKILL.md
  • vitest.shared.ts

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • Review on demand using usage pricing
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/protect-ffi-0.30

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Comment thread packages/prisma-next/package.json
Comment thread packages/stack-supabase/src/query-builder-v3.ts
Comment thread examples/prisma/migrations/app/20260714T2142_initial/ops.json
@changeset-bot

changeset-bot Bot commented Jul 20, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 434975a

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 11 packages
Name Type
@cipherstash/stack Minor
@cipherstash/stack-drizzle Minor
@cipherstash/stack-supabase Minor
@cipherstash/prisma-next Minor
stash Minor
@cipherstash/bench Patch
@cipherstash/test-kit Patch
@cipherstash/basic-example Patch
@cipherstash/prisma-next-example Patch
@cipherstash/e2e Patch
@cipherstash/wizard Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@freshtonic freshtonic left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overview

Upgrades Stack to @cipherstash/protect-ffi 0.30 and EQL 3.0.2, and lands the real feature: GIN-indexable JSON-path equality plus ciphertext-free JSON-path ordering (the new steVecValueSelector op) across Drizzle and Prisma Next, a versioned EQL 3.0.2 upgrade migration, and three fail-closed hardening changes (short free-text needles rejected at the FFI boundary; legacy v2 searchableJson() rejected at client setup; Supabase fails fast for operators PostgREST can't express).

Given the breadth (117 files, four packages, an FFI+EQL bump), I read the core boundary and changeset myself and ran four focused verifications — core stack, Supabase fail-closed, Drizzle+Prisma-Next operator routing, and migrations/deps/changeset. No confirmed bugs surfaced anywhere. Approving.

What I verified

Security posture is fail-closed throughout — the strongest part of this PR:

  • Short free-text needle rejection happens in protect-ffi 0.30 on the query-encrypt path and rejects (not warns) below the ngram token_length (3), closing the stored_bf @> '{}' match-all fail-open (#697). Pinned by a live match-bloom test.
  • Legacy v2 searchableJson() is rejected in resolveEqlVersion (stack/src/encryption/index.ts) at the Encryption() factory — before any FFI client is constructed, and even when eqlVersion: 2 is set explicitly — with a clear steer to types.Json(). Correctly gated on v3Count === 0 (v3 columns never trip it) and tested (asserts throw + newClient not called).
  • Supabase fail-fast (query-builder-v3.ts): matches(), encrypted contains(), selectorEq()/selectorNe() throw an actionable error (naming Drizzle/Prisma-Next/scoped-RPC) rather than emitting a query PostgREST would run incorrectly. Version detection reads the eql_v3 schema comment and fails closed on unknown/unparseable (→ true); the anchored ^\d+\.\d+\.\d+ regex is safe because v3 versions are bare (no eql- prefix hazard). A common async term-path guard catches every raw filter spelling (filter('cs'), not('matches'), or([...])). No over-triggering on equality/order/plaintext contains.

The operator rewrite is a real improvement, not a risky correctness change. Drizzle and Prisma Next lower identically (eq/ne → @> value-selector containment over eql_v3.query_json; ordering → eql_v3.{gt,gte,lt,lte} over jsonb_path_query_first + query_text_ord/query_double_ord), the type surface exactly matches the six runtime eqlJsonPath* operators (no orphans either way), and selector-path.ts leaf validation now correctly rejects Date/bigint/non-finite numbers with actionable errors. Crucially: the old paths were functionally correct but placed a storage ciphertext c in the WHERE clause and couldn't engage a GIN index — the rewrite removes ciphertext from the predicate and makes equality index-answerable. Net: better, and no silent wrong-results regression.

Migration + release hygiene is clean. The new 20260720T0000_upgrade_eql_v3_3_0_2 edge is an invariant-only self-edge that chains onto head (head's invariant list grows, graph stays reachable, example mirror consistent). The upgrade SQL is data-safe: it drops/recreates only EQL's own eql_v3/eql_v3_internal schemas — zero DROP TABLE/TRUNCATE/DELETE/ALTER on user tables (re-encryption of SteVec rows is correctly left as a runtime concern). No protect-ffi/eql version skew among the bumped packages; the lockfile is fully updated (--frozen-lockfile-compatible); onlyBuiltDependencies unchanged (protect-ffi ships prebuilt). The single changeset covers every changed published package (stack/drizzle/prisma-next minor, supabase/stash patch) with all three breaking notes captured; minor on stack is consistent with the rc prerelease train.

Notes — non-blocking

  1. steVecTerm public QueryTypeName semantics flipped from containment → ordering. The recommended auto-inferring searchableJson path handles this internally (object/array now routes to a new default containment op), so real-world impact is low, but anyone who explicitly passed queryType: 'steVecTerm' expecting containment would now get an ordering term and silently different results. Worth an explicit line in the upgrade notes for the explicit-queryType path (v3 is pre-GA, so acceptable).
  2. Two test-coverage gaps worth a cheap follow-up: the short-needle rejection is pinned only by a credential-gated live test (won't run in no-creds CI) — given it closes a security fail-open, a non-live unit pin on the length guard would be valuable; and unsupportedLeafReason has no direct unit test in stack (exercised only via the adapter packages).
  3. Pre-existing, out of scope: packages/protect still pins protect-ffi 0.23.0 while stack/drizzle/supabase move to 0.30.0, so the repo genuinely resolves two protect-ffi majors. Not touched by this PR and @cipherstash/stack doesn't depend on protect, but worth tracking given the version-skew lesson from the rc.2 B1 bug.
  4. Cosmetic: the ne/neq SQL differs in operand order between the two adapters ((NOT contains OR col IS NULL) vs (col IS NULL OR NOT (...))) — logically identical and precedence-safe; and the supabase SKILL.md filter table still lists match() (PostgREST's regex operator, distinct from the removed matches()) — pre-existing.

Verdict

Approve. A large but disciplined upgrade: the fail-closed hardening is correct and closes a real match-all fail-open, the JSON-path operators lower identically across adapters with an exact type↔runtime match and now keep ciphertext out of the WHERE clause, the upgrade migration is data-safe, and dependency/changeset hygiene is clean with no version skew. All four notes are non-blocking follow-ups; item 1 (documenting the steVecTerm semantics flip) and item 2 (a non-live unit pin for the needle guard) are the two most worth picking up.

@coderdan

coderdan commented Jul 20, 2026 •

Copy link
Copy Markdown
Contributor Author

Addressed the actionable follow-up feedback in signed commit 5ec5be02:

  • Documented the steVecTerm semantic change for raw encryptQuery callers in the changeset, Stack README, and bundled encryption skill.
  • Added a credential-free core v3 preflight that rejects match needles which cannot produce an n-gram before calling protect-ffi, covering both scalar and batch query encryption.
  • Added direct unit coverage for unsupportedLeafReason, including composite values, non-finite numbers, bigint, Date, nullish values, and boolean ordering.
  • Added scalar and batch tests asserting short match needles do not reach the FFI.

I left the two notes explicitly identified as pre-existing/out of scope or cosmetic unchanged.

Validation:

  • Stack: 809 tests
  • Supabase: 488 tests + 33 type tests
  • Drizzle: 374 tests
  • Prisma Next: 610 tests + TypeScript typecheck
  • Stack build and repository code check

@coderdan

Copy link
Copy Markdown
Contributor Author

Addressed the JSON integration parity feedback in verified commit a0b86943:

  • Added selector-based ORDER BY to Drizzle as ops.selector(column, path).asc()/desc().
  • Added Prisma Next parity as eqlJsonPathAsc(column, path) / eqlJsonPathDesc(column, path).
  • Both lower to the canonical, functional-index-matchable form eql_v3.ord_term(column -> selector::text).
  • Replaced the separate Drizzle JSON suites with one shared test-kit contract covering containment, scalar selector equality/ranges, absent paths, invalid values/paths, and ascending/descending selector ordering.
  • The identical live suite now runs against Drizzle and Prisma Next. Supabase keeps its separate EQL 3.0.2/PostgREST rejection-boundary suite because it cannot express typed JSON query operands.

Local validation passed: Drizzle 377 tests + declaration tests + build; Prisma Next 611 tests + typecheck + build; test-kit typecheck; repository code check. The shared credentialed live suites are running in CI.

@coderdan
coderdan force-pushed the codex/protect-ffi-0.30 branch from 9531a95 to 434975a Compare July 21, 2026 03:16
@coderdan
coderdan merged commit cf2c57c into main Jul 21, 2026
19 of 20 checks passed
@coderdan
coderdan deleted the codex/protect-ffi-0.30 branch July 21, 2026 03:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants