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

feat: support protect-ffi 0.30 JSON queries#711
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

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