Skip to content

feat(stack,cli): EQL v3 typed schema, typed client, Supabase/Drizzle integrations on eql-3.0.0 + protect-ffi 0.29#535

Open
tobyhede wants to merge 241 commits into
mainfrom
feat/eql-v3-text-search-schema
Open

feat(stack,cli): EQL v3 typed schema, typed client, Supabase/Drizzle integrations on eql-3.0.0 + protect-ffi 0.29#535
tobyhede wants to merge 241 commits into
mainfrom
feat/eql-v3-text-search-schema

Conversation

@tobyhede

@tobyhede tobyhede commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

EQL v3 typed schema + strongly-typed client (@cipherstash/stack/eql/v3, @cipherstash/stack/v3)

🚦 Stack 1.0 Release Candidate

This is the integration/candidate branch for Stack 1.0 (feat/eql-v3-text-search-schema).
It bundles the EQL v3 surface described below plus the stacked work listed at the
end, and is merged current with main. Treat it as the 1.0-RC: the API surface,
payload shapes, and CLI commands here are the ones intended to ship as 1.0.

In the RC (merged into this branch): the typed v3 schema + client, the
Supabase and Drizzle v3 integrations, EQL v3 JSON (types.Json + containment),
the real-crypto integration harness (#616), envelope/Result type robustness
(#618), live identity suites (#620), the include_original substring correction
(#615), and the type-erasing-assertion lint gate (#624). See Stacked work below.

Landing alongside the RC (open, based on this branch): #606, #604, #586, #526.

Post-1.0 follow-ups (tracked, not blocking the RC): adapter package split
(#627), Supabase encryptionError fix (#626), unit no-skip enforcement (#628),
superuser ORE SQL coverage (#629), and upstream include_original handling
(cipherstash/protectjs-ffi#134).

Builds the full EQL v3 authoring and querying surface on the eql-3.0.0 GA release and protect-ffi 0.29: a types namespace with one member per modelled EQL v3 SQL domain, a strongly-typed v3 client (@cipherstash/stack/v3) that derives input/output types from the schema and rejects misuse at compile time, an EQL v3 Supabase adapter with database introspection, an EQL v3 Drizzle integration, and the CLI install path (stash eql install --eql-version 3). v2 is unchanged; pick the model by import path.

Usage

import { Encryption } from "@cipherstash/stack";
import { encryptedTable, types } from "@cipherstash/stack/eql/v3";

// 1. Define your schema
const users = encryptedTable("users", {
  email: types.TextSearch("email"),
});

// 2. Initialize the client (v3 wire format is auto-detected from the schema)
const client = await Encryption({ schemas: [users] });

// 3. Encrypt
const encryptResult = await client.encrypt("secret@example.com", {
  column: users.email,
  table: users,
});
if (encryptResult.failure) {
  // Handle errors your way
}

// 4. Decrypt
const decryptResult = await client.decrypt(encryptResult.data);
// decryptResult.data => "secret@example.com"

Mix any v3 domains in one table — each column declares its own type and query capabilities. The types.* member name maps 1:1 to the flat public.eql_v3_<name> domain (strip the eql_v3_ prefix, PascalCase each segment):

import { encryptedTable, types } from "@cipherstash/stack/eql/v3";

const events = encryptedTable("events", {
  actor:     types.TextEq("actor"),          // equality      → public.eql_v3_text_eq
  weight:    types.IntegerOrd("weight"),     // order + range → public.eql_v3_integer_ord
  views:     types.BigintOrd("views"),       // JS bigint     → public.eql_v3_bigint_ord
  createdAt: types.Timestamp("created_at"),  // storage only  → public.eql_v3_timestamp
});

Plaintext types are inferred per domain; Date and bigint values work directly (bigint is marshalled losslessly by protect-ffi 0.28+, with i64 bounds enforced before the FFI):

import type { InferPlaintext } from "@cipherstash/stack/eql/v3";

type Events = InferPlaintext<typeof events>;
// { actor: string; weight: number; views: bigint; createdAt: Date }

Queryability is enforced at compile time — storage-only columns can't be queried.

types members & capabilities

One member per modelled EQL v3 domain. Each maps to an EQL v3 SQL type and exposes getQueryCapabilities() / isQueryable(). Every domain is fully described by its type — there is no capability-bearing or tuning chain (the former .freeTextSearch(opts) tuner is gone; match indexes use the default configuration).

Suffix Example member EQL v3 domain Capabilities
(none) types.Integer public.eql_v3_integer storage only
Eq types.TextEq public.eql_v3_text_eq equality
Ord, OrdOre types.IntegerOrd public.eql_v3_integer_ord equality + order/range
TextMatch types.TextMatch public.eql_v3_text_match free-text
TextSearch types.TextSearch public.eql_v3_text_search equality + order/range + free-text

Covered families (40 domains): integer/smallint/bigint, real/double, numeric, date, timestamp, text*, boolean → inferred as number / bigint / Date / string / boolean. The DOMAIN_REGISTRY derives itself from these factories and keys Supabase introspection.

Not yet modelled (deliberate follow-up): the *_ord_ope twins and text_search_ore — they exist in the eql-3.0.0 SQL and protect-ffi 0.29 supports the ope index, but the SDK surface, matrix coverage, and introspection classification for them are a separate increment. Introspection reports them as unmodelled EQL columns and guards queries against them.

Strongly-typed client (@cipherstash/stack/v3)

EncryptionV3 mirrors Encryption; typedClient retypes an existing client. Both re-export the v3 types namespace and table API.

  • encrypt / encryptQuery pin the plaintext to the column's domain type and constrain queryType to the column's capabilities at compile time.
  • encryptModel / bulkEncryptModels validate schema-column fields against their inferred plaintext type; passthrough fields are untouched.
  • decryptModel / bulkDecryptModels return the precise plaintext model, reconstructing Date values (date and timestamp casts) via per-table reconstructors precomputed at construction.
  • EncryptionV3 pins eqlVersion: 3 (a v2-mode client cannot resolve v3 concrete-type columns); Encryption auto-detects v3 tables and rejects mixed v2 + v3 schema sets — one client emits exactly one wire format.

Supabase adapter (encryptedSupabaseV3)

An EQL v3 dialect of the Supabase integration: introspects the database over the optional pg peer (EQL v3 domains classified via DOMAIN_REGISTRY), synthesizes or verifies declared schemas, expands select('*') from the introspected column list, aliases renamed columns back to their JS property names with ::jsonb casts, encrypts filter operands (full storage envelopes — PostgREST cannot cast a filter value to the eql_v3.query_<name> twins), translates contains() to PostgREST cs bloom containment (like/ilike are rejected on encrypted columns by design), hardens .or() string parsing/rebuilding, and guards unmodelled EQL domains. Null-prototype column maps close the __proto__/constructor lookup holes.

Drizzle integration (@cipherstash/stack/eql/v3/drizzle)

makeEqlV3Column (drizzle customType emitting the domain DDL), extractEncryptionSchemaV3, and createEncryptionOperatorsV3 (eq/gt/gte/lt/lte/between/inArray/notInArray/contains — no like/ilike; operands are full envelopes compared via the two-arg eql_v3.* functions).

CLI

stash eql install --eql-version 3 installs the eql-3.0.0 bundle, vendored from the pinned @cipherstash/eql@3.0.0 npm package and sha256-verified against its release manifest. Since 3.0.0 one artifact installs everywhere: operator-class statements self-skip on insufficient_privilege and the bundle disables ORE-backed domains when the opclass is absent (CIP-3468), so the separate v3 Supabase variant is gone. --supabase still applies the role grants (eql_v3 and eql_v3_internal — the SECURITY INVOKER extractors need both). isInstalled is generation-aware and treats a stale pre-GA install as not installed.

protect-ffi 0.29 / eql-3.0.0 re-baseline

  • Column domains renamed to the GA convention: flat public.eql_v3_<name>. Query domains stay eql_v3.query_<name>. Databases installed from an alpha bundle must be re-installed (the bundle replaces the schema).
  • v3 scalar/selector encryptQuery no longer throws — protect-ffi 0.29 mints term-only query operands (EQL_V3_QUERY_UNSUPPORTED is gone). The adapters keep full-envelope operands where SQL-side casting isn't available (PostgREST).
  • v2 protection: protect-ffi 0.29 flipped the SteVec default encoding to compat (EQL v3's op term). The v2 searchableJson() builders (stack + @cipherstash/schema) now pin mode: 'standard' so existing v2 encrypted JSON columns stay queryable and the v2 wire format stays byte-stable.
  • Test/install SQL comes from @cipherstash/eql (release-manifest-verified) instead of a hand-vendored fixture; installEqlV3IfNeeded checks eql_v3.version() against the pinned release.

Docs & skills (ship in the stash tarball)

skills/stash-encryption gains the full EQL v3 typed-schema section; skills/stash-drizzle gains the eql/v3/drizzle section; skills/stash-supabase and docs/reference/supabase-sdk.md are re-synced against the current adapter (introspecting factory, contains(), single install artifact, GA naming).

Notes

Live client/pg tests require CipherStash credentials (CI provides them; the require-cs-secrets action fails loudly when missing). The supabase-v3-grants-pg / supabase-v3-introspect-pg suites are flaky over a PgBouncer transaction-mode pooler (session advisory locks + SET ROLE) — they are written for the CI service Postgres.


Stacked work on this candidate branch (1.0)

This branch is the integration/candidate for Stack 1.0. Beyond the core typed
schema + client above, the following has been merged into it:

Open PRs based on this branch: #606 (stash-supabase skill, V3-first rewrite),
#604 (stash-drizzle skill + eq/ne contract), #586 (CLI defaults EQL to v3, stops
recommending db push), #526 (root README refresh, draft).

Follow-ups filed: cipherstash/protectjs-ffi#134 (include_original inert —
honour or reject it), #626 (Supabase v2 builder discards encryptionError), and
the adapter package split (see the tracking issue) that would move the Drizzle
and Supabase integrations into their own packages like @cipherstash/prisma-next.

@tobyhede tobyhede requested a review from a team as a code owner June 30, 2026 22:27
@changeset-bot

changeset-bot Bot commented Jun 30, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: ba2ccc1

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

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

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

@coderabbitai

coderabbitai Bot commented Jun 30, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR adds a v3 text_search schema builder API, widens encryption/query types to accept structural builders, wires export and typecheck support, and updates CLI dotenv loading plus E2E test execution.

Changes

EQL v3 text_search schema DSL and client type widening

Layer / File(s) Summary
Planning and design documentation
.changeset/eql-v3-text-search.md, .changeset/eql-v3-typed-schema.md, docs/superpowers/plans/..., docs/superpowers/specs/...
Adds a minor changeset note plus implementation plan and design spec documents for the v3 text_search DSL, compatibility constraints, and testing expectations.
v3 text_search schema builders
packages/stack/src/schema/v3/index.ts
Implements EncryptedTextSearchColumn, encryptedTextSearchColumn, v3 EncryptedTable/encryptedTable, buildEncryptConfig, and v3 inference helpers with deep-cloned build output and reserved-key collision checks.
Public client and operation type widening
packages/stack/src/types.ts, packages/stack/src/schema/index.ts, packages/stack/src/encryption/helpers/infer-index-type.ts, packages/stack/src/encryption/helpers/model-helpers.ts, packages/stack/src/encryption/index.ts, packages/stack/src/encryption/operations/*
Adds BuildableColumn, BuildableQueryColumn, and BuildableTable, updates client config and query/encrypt option types, and retypes the encrypt, bulk-encrypt, schema, and index-inference helpers to accept structural builders.
Structural column-name resolution
packages/stack/src/wasm-inline.ts, packages/stack/__tests__/wasm-inline-column-name.test.ts
Exports getColumnName and changes it to validate columns structurally through getName(), removing the previous instanceof-based checks for v2-only encrypted column classes.
Export wiring and typecheck pipeline
packages/stack/package.json, packages/stack/tsup.config.ts, packages/stack/tsconfig.typecheck.json, packages/stack/vitest.config.ts, .github/workflows/tests.yml, packages/stack/scripts/install-eql-v3.ts
Adds the ./schema/v3 package export and type mappings, includes the v3 entry in tsup, and wires scoped typecheck config, Vitest typecheck settings, a test:types script, and the CI step that runs it.
Runtime and type-level acceptance tests
packages/stack/__tests__/schema-v3.test.ts, packages/stack/__tests__/schema-v3.test-d.ts, packages/stack/__tests__/schema-v3-client.test.ts, packages/stack/__tests__/schema-v3-pg.test.ts, packages/stack/__tests__/cjs-require.test.ts, packages/stack/__tests__/helpers/*
Adds runtime tests for the v3 schema builders and type-level tests for v3 schema inference and client integration, plus live Postgres/CJS helper coverage and the negative queryability check.

CLI dotenv loading and non-PTY test helper

Layer / File(s) Summary
CLI dotenv loading and helper
packages/cli/src/bin/main.ts, packages/cli/tests/helpers/run.ts
Loads .env* files with quiet: true, adds the pipe-based run helper, and exports result types for CLI test execution.
CLI E2E updates
packages/cli/tests/e2e/runner-aware-help.e2e.test.ts, packages/cli/tests/e2e/smoke.e2e.test.ts
Switches runner-aware help tests to the new run helper and adds a smoke-test assertion that the dotenv banner is absent from help output.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related issues

Possibly related PRs

  • cipherstash/stack#493: Shares the same encryption query/bulk typing surface and the Plaintext widening work.
  • cipherstash/stack#496: Closely related to the structural getColumnName() change in packages/stack/src/wasm-inline.ts.
  • cipherstash/stack#497: Shares the same encryption operation code paths and lock-context typing changes.

Suggested reviewers: calvinbrewer

Poem

A bunny hopped through schemas bright,
With v3 text search taking flight 🐇
New builders bloom, the types align,
And CLI banners settle fine.
Thump! The tests now run with cheer,
Quiet envs and clean paths here.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 78.57% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly captures the main change: adding EQL v3 typed schema and widening the typed client surface.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/eql-v3-text-search-schema

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
packages/stack/src/types.ts (1)

111-113: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Possible redundant union arm.

If EncryptedColumn already has a public getEqlType() method and no private/protected members forcing nominal typing, the explicit EncryptedColumn arm is structurally subsumed by BuildableColumn & { getEqlType(): string } and could be dropped for simplicity. Not a functional issue either way; only worth simplifying if confirmed redundant.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/stack/src/types.ts` around lines 111 - 113, The BuildableQueryColumn
union appears to have a redundant arm if EncryptedColumn already satisfies
BuildableColumn & { getEqlType(): string } structurally. Check the
EncryptedColumn type and, if it does not rely on nominal typing via
private/protected members, simplify the BuildableQueryColumn alias in types.ts
by removing the explicit EncryptedColumn union member and keeping only the
shared structural form.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/stack/src/types.ts`:
- Around line 111-113: The BuildableQueryColumn union appears to have a
redundant arm if EncryptedColumn already satisfies BuildableColumn & {
getEqlType(): string } structurally. Check the EncryptedColumn type and, if it
does not rely on nominal typing via private/protected members, simplify the
BuildableQueryColumn alias in types.ts by removing the explicit EncryptedColumn
union member and keeping only the shared structural form.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 01405e99-c5e9-4153-8af3-10759fd8ebbd

📥 Commits

Reviewing files that changed from the base of the PR and between 93fc5f9 and efe4cc0.

📒 Files selected for processing (16)
  • .changeset/eql-v3-text-search.md
  • .github/workflows/tests.yml
  • docs/superpowers/plans/2026-06-30-eql-v3-text-search-schema-plan.md
  • docs/superpowers/specs/2026-06-30-eql-v3-text-search-schema-design.md
  • packages/stack/__tests__/schema-v3.test-d.ts
  • packages/stack/__tests__/schema-v3.test.ts
  • packages/stack/package.json
  • packages/stack/src/encryption/helpers/infer-index-type.ts
  • packages/stack/src/encryption/operations/bulk-encrypt.ts
  • packages/stack/src/encryption/operations/encrypt.ts
  • packages/stack/src/schema/index.ts
  • packages/stack/src/schema/v3/index.ts
  • packages/stack/src/types.ts
  • packages/stack/tsconfig.typecheck.json
  • packages/stack/tsup.config.ts
  • packages/stack/vitest.config.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
packages/cli/tests/helpers/run.ts (1)

44-87: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

No timeout/kill safeguard for a hung child.

If the spawned CLI process hangs (e.g., waiting on unexpected input despite stdio: ['ignore', ...]), nothing here kills it — the test will eventually time out via vitest, but the orphaned child process keeps running. Consider an optional timeout that calls child.kill() and rejects.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/tests/helpers/run.ts` around lines 44 - 87, The run helper
currently waits indefinitely for the spawned CLI in run() and only resolves on
close, so a hung child can outlive the test. Update
packages/cli/tests/helpers/run.ts by adding an optional timeout to RunOptions
and wiring it in run() to call child.kill() and reject if the process does not
exit in time, while preserving the existing stdout/stderr capture and cleanup in
the child.on('close') path.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/cli/tests/helpers/run.ts`:
- Around line 44-87: The run helper currently waits indefinitely for the spawned
CLI in run() and only resolves on close, so a hung child can outlive the test.
Update packages/cli/tests/helpers/run.ts by adding an optional timeout to
RunOptions and wiring it in run() to call child.kill() and reject if the process
does not exit in time, while preserving the existing stdout/stderr capture and
cleanup in the child.on('close') path.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8faee085-09ef-4411-822e-50addd54c10c

📥 Commits

Reviewing files that changed from the base of the PR and between 32707e2 and 30cd5f4.

📒 Files selected for processing (5)
  • packages/cli/src/bin/main.ts
  • packages/cli/tests/e2e/runner-aware-help.e2e.test.ts
  • packages/cli/tests/e2e/smoke.e2e.test.ts
  • packages/cli/tests/helpers/run.ts
  • packages/stack/src/schema/v3/index.ts
✅ Files skipped from review due to trivial changes (1)
  • packages/cli/tests/e2e/smoke.e2e.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/stack/src/schema/v3/index.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/stack/src/schema/v3/index.ts (1)

460-470: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Snapshot nested freeTextSearch() options when you store them.

Lines 465-466 keep opts.tokenizer and opts.token_filters by reference, so mutating the caller’s options object after configuration silently changes this builder’s later build() output. The rest of this class is explicitly avoiding shared nested state, so this should clone on write too.

Suggested fix
   freeTextSearch(opts?: MatchIndexOpts): this {
     // A fresh defaults object per call supplies the `?? ` fallbacks, so no
     // nested default object is ever shared into `this.matchOpts` by reference.
     const defaults = defaultMatchOpts()
 
     this.matchOpts = {
-      tokenizer: opts?.tokenizer ?? defaults.tokenizer,
-      token_filters: opts?.token_filters ?? defaults.token_filters,
+      tokenizer: opts?.tokenizer
+        ? { ...opts.tokenizer }
+        : { ...defaults.tokenizer },
+      token_filters: opts?.token_filters
+        ? opts.token_filters.map((f) => ({ ...f }))
+        : defaults.token_filters.map((f) => ({ ...f })),
       k: opts?.k ?? defaults.k,
       m: opts?.m ?? defaults.m,
       include_original: opts?.include_original ?? defaults.include_original,
     }
     return this
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/stack/src/schema/v3/index.ts` around lines 460 - 470, The
freeTextSearch method in the v3 schema builder is still storing nested options
by reference, so later mutations to the caller’s MatchIndexOpts can leak into
build() output. Update freeTextSearch in packages/stack/src/schema/v3/index.ts
to clone the tokenizer and token_filters values when assigning this.matchOpts,
matching the class’s existing clone-on-write approach and avoiding shared nested
state.
🧹 Nitpick comments (6)
docs/superpowers/plans/2026-07-01-eql-v3-typed-schema.md (3)

510-510: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Fix redundant phrasing.

"Repeat the same exact pattern" → "Repeat the same pattern" or "Repeat this exact pattern".

- Repeat the same exact pattern for:
+ Repeat the same pattern for:
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/superpowers/plans/2026-07-01-eql-v3-typed-schema.md` at line 510, Update
the phrasing in the plans document to remove redundancy: change the “Repeat the
same exact pattern for:” text to either “Repeat the same pattern for:” or
“Repeat this exact pattern for:”. Locate the sentence in the document section
containing that exact phrase and keep the rest unchanged.

Source: Linters/SAST tools


117-117: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider rewording for readability.

Three successive list items begin with "Schemas with". While this is a list format where parallelism is expected, consider varying the structure if the static analysis tool flagged it as an issue.

- - Schemas with required `hm` support equality.
- - Schemas with required `ob` support order/range.
- - Schemas with required `bf` support free-text search.
+ - `hm` required → equality support.
+ - `ob` required → order/range support.
+ - `bf` required → free-text search support.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/superpowers/plans/2026-07-01-eql-v3-typed-schema.md` at line 117, Reword
the list item about storage-only schemas to avoid repeating the same “Schemas
with” sentence pattern as the surrounding bullets. Update the wording in the
schema documentation section so it still conveys that schemas containing only v,
i, and c are storage-only, but uses a different sentence structure for
readability and to satisfy the static analysis warning.

Source: Linters/SAST tools


27-27: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider rewording for readability.

Three successive sentences beginning with "In" - though this appears to be in the file structure list where parallelism is intentional. Given the context is a structured plan document, this is acceptable but could be tightened.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/superpowers/plans/2026-07-01-eql-v3-typed-schema.md` at line 27, The
plan document wording is a bit repetitive in the file-structure list, where
several consecutive bullets/sentences start with the same “In” pattern. Rephrase
the affected entries in the schema-plan section to improve readability while
preserving the parallel structure, keeping the “BuildableTable” bullet clear and
concise.

Source: Linters/SAST tools

packages/stack/scripts/install-eql-v3.ts (1)

1-1: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Bare dotenv/config re-introduces the banner noise this cohort suppresses elsewhere.

The CLI entrypoint now loads env files with config({ path: '.env.local', quiet: true }) specifically to avoid dotenv v17's injected-env banner. This script imports dotenv/config directly with no options, so it will still print that banner whenever it runs (e.g. in CI logs).

♻️ Suggested fix
-import 'dotenv/config'
+import { config } from 'dotenv'
 import postgres from 'postgres'
 import { installEqlV3IfNeeded } from '../__tests__/helpers/eql-v3'
+
+config({ quiet: true })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/stack/scripts/install-eql-v3.ts` at line 1, The install-eql-v3
script is importing dotenv in a way that re-enables the noisy injected-env
banner. Update the startup env loading in this script to use the same explicit
dotenv config pattern as the CLI entrypoint, with a fixed .env.local path and
quiet enabled, and remove the bare dotenv/config import so the script stays
silent in CI. Reference the script’s top-level env bootstrap in
install-eql-v3.ts.
packages/stack/__tests__/schema-v3.test.ts (1)

2-3: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Exercise this failure through a public API instead of resolveIndexType.

This test now imports @/encryption/helpers/infer-index-type directly and asserts the helper’s internal error strings, which makes the suite brittle to refactors inside the implementation rather than the supported contract. Please move this misuse coverage to the public entry point that surfaces the same runtime failure. As per coding guidelines, "Prefer testing via public API; avoid reaching into private internals in tests".

Also applies to: 661-677

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/stack/__tests__/schema-v3.test.ts` around lines 2 - 3, The test is
reaching into the private `resolveIndexType` helper and asserting its internal
error strings, which makes it brittle. Update
`packages/stack/__tests__/schema-v3.test.ts` to remove the direct
`@/encryption/helpers/infer-index-type` import and exercise the same failure
through the public `encryptConfigSchema`/`encryptedColumn` API instead. Keep the
coverage for the misuse case, but assert the runtime failure surfaced by the
supported schema entry point rather than helper internals.

Source: Coding guidelines

packages/stack/__tests__/schema-v3.test-d.ts (1)

229-301: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a model-inference case with aliased column names.

This file already proves v3 domain inference for aliased builders like createdAtcreated_at, but the encryptModel / bulkEncryptModels acceptance cases only cover same-name keys. One typed assertion for an aliased encryptedTimestamptzColumn('created_at') model field would protect the exact v3 field mapping this PR is widening.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/stack/__tests__/schema-v3.test-d.ts` around lines 229 - 301, Add a
typed model-inference test for aliased v3 encrypted columns, since the current
`encryptModel` and `bulkEncryptModels` cases only cover same-name fields. Update
the `schema-v3.test-d.ts` assertions near the existing
`encryptModel`/`bulkEncryptModels` checks to include a model using
`encryptedTimestamptzColumn('created_at')` with an aliased property name. Verify
the inferred `EncryptionClient.encryptModel` and/or `bulkEncryptModels` result
type maps the aliased field correctly while still preserving unrelated fields.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/superpowers/plans/2026-07-01-eql-v3-typed-schema.md`:
- Around line 63-65: The task steps currently hardcode a developer-specific
absolute path in the read-only references, which will not work for other
environments. Update the instructions in the plan to use repository-relative
paths or an environment-agnostic placeholder, and if needed mention that the
base path must be configured by the user; keep the references to the
inventory.rs and schema/v3/*.json locations clear without embedding a local
machine path.

In `@packages/stack/__tests__/helpers/eql-v3.ts`:
- Around line 28-41: The advisory lock handling in installEqlV3IfNeeded is using
separate sql calls, so the lock and unlock may run on different pooled
connections. Update the function to run the entire check/install/unlock flow on
a reserved connection via sql.reserve(), or replace
pg_advisory_lock/pg_advisory_unlock with pg_advisory_xact_lock if the EQL v3
install path is transaction-safe, and keep the existing hasEqlV3TextSearch and
eqlV3Sql execution logic inside that reserved scope.

In `@packages/stack/__tests__/schema-v3-pg.test.ts`:
- Around line 133-149: The cleanup hooks in the schema-v3-pg test only remove
rows from protect_ci_v3_text_search, so the typed-domain fixture data in
protect_ci_v3_typed_domains is left behind. Update the existing beforeEach and
afterAll hooks in schema-v3-pg.test.ts to also delete rows for the typed-domain
table using the same TEST_RUN_ID guard, alongside the current cleanup logic. Use
the existing beforeEach, afterAll, and sql cleanup blocks as the place to add
the matching protect_ci_v3_typed_domains deletion.

In `@packages/stack/src/types.ts`:
- Around line 151-183: The public BuildableTable shape is too weak for the
encryption inference used by encryptModel() and bulkEncryptModels(), so
structurally accepted tables lose the literal column keys needed by
EncryptedFromBuildableTable. Fix this by either adding the column map
brand/_columnType to the BuildableTable contract and keeping
BuildableTableColumns aligned with it, or by narrowing the affected APIs/types
back to the branded table builder type so the return type reflects encrypted
fields correctly.

---

Outside diff comments:
In `@packages/stack/src/schema/v3/index.ts`:
- Around line 460-470: The freeTextSearch method in the v3 schema builder is
still storing nested options by reference, so later mutations to the caller’s
MatchIndexOpts can leak into build() output. Update freeTextSearch in
packages/stack/src/schema/v3/index.ts to clone the tokenizer and token_filters
values when assigning this.matchOpts, matching the class’s existing
clone-on-write approach and avoiding shared nested state.

---

Nitpick comments:
In `@docs/superpowers/plans/2026-07-01-eql-v3-typed-schema.md`:
- Line 510: Update the phrasing in the plans document to remove redundancy:
change the “Repeat the same exact pattern for:” text to either “Repeat the same
pattern for:” or “Repeat this exact pattern for:”. Locate the sentence in the
document section containing that exact phrase and keep the rest unchanged.
- Line 117: Reword the list item about storage-only schemas to avoid repeating
the same “Schemas with” sentence pattern as the surrounding bullets. Update the
wording in the schema documentation section so it still conveys that schemas
containing only v, i, and c are storage-only, but uses a different sentence
structure for readability and to satisfy the static analysis warning.
- Line 27: The plan document wording is a bit repetitive in the file-structure
list, where several consecutive bullets/sentences start with the same “In”
pattern. Rephrase the affected entries in the schema-plan section to improve
readability while preserving the parallel structure, keeping the
“BuildableTable” bullet clear and concise.

In `@packages/stack/__tests__/schema-v3.test-d.ts`:
- Around line 229-301: Add a typed model-inference test for aliased v3 encrypted
columns, since the current `encryptModel` and `bulkEncryptModels` cases only
cover same-name fields. Update the `schema-v3.test-d.ts` assertions near the
existing `encryptModel`/`bulkEncryptModels` checks to include a model using
`encryptedTimestamptzColumn('created_at')` with an aliased property name. Verify
the inferred `EncryptionClient.encryptModel` and/or `bulkEncryptModels` result
type maps the aliased field correctly while still preserving unrelated fields.

In `@packages/stack/__tests__/schema-v3.test.ts`:
- Around line 2-3: The test is reaching into the private `resolveIndexType`
helper and asserting its internal error strings, which makes it brittle. Update
`packages/stack/__tests__/schema-v3.test.ts` to remove the direct
`@/encryption/helpers/infer-index-type` import and exercise the same failure
through the public `encryptConfigSchema`/`encryptedColumn` API instead. Keep the
coverage for the misuse case, but assert the runtime failure surfaced by the
supported schema entry point rather than helper internals.

In `@packages/stack/scripts/install-eql-v3.ts`:
- Line 1: The install-eql-v3 script is importing dotenv in a way that re-enables
the noisy injected-env banner. Update the startup env loading in this script to
use the same explicit dotenv config pattern as the CLI entrypoint, with a fixed
.env.local path and quiet enabled, and remove the bare dotenv/config import so
the script stays silent in CI. Reference the script’s top-level env bootstrap in
install-eql-v3.ts.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4aa02b84-db93-4a9b-aa84-d185e2c884d9

📥 Commits

Reviewing files that changed from the base of the PR and between 30cd5f4 and b54e6d4.

📒 Files selected for processing (25)
  • .changeset/eql-v3-typed-schema.md
  • docs/query-api-walkthrough.md
  • docs/superpowers/plans/2026-07-01-eql-v3-typed-schema.md
  • packages/stack/__tests__/cjs-require.test.ts
  • packages/stack/__tests__/fixtures/eql-v3/cipherstash-encrypt-v3.sql
  • packages/stack/__tests__/helpers/eql-v3.ts
  • packages/stack/__tests__/helpers/stub-auth-wasm-inline.ts
  • packages/stack/__tests__/helpers/stub-protect-ffi-wasm-inline.ts
  • packages/stack/__tests__/schema-v3-client.test.ts
  • packages/stack/__tests__/schema-v3-pg.test.ts
  • packages/stack/__tests__/schema-v3.test-d.ts
  • packages/stack/__tests__/schema-v3.test.ts
  • packages/stack/__tests__/wasm-inline-column-name.test.ts
  • packages/stack/package.json
  • packages/stack/scripts/install-eql-v3.ts
  • packages/stack/src/encryption/helpers/infer-index-type.ts
  • packages/stack/src/encryption/helpers/model-helpers.ts
  • packages/stack/src/encryption/index.ts
  • packages/stack/src/encryption/operations/bulk-encrypt-models.ts
  • packages/stack/src/encryption/operations/encrypt-model.ts
  • packages/stack/src/encryption/operations/encrypt-query.ts
  • packages/stack/src/encryption/operations/encrypt.ts
  • packages/stack/src/schema/v3/index.ts
  • packages/stack/src/types.ts
  • packages/stack/vitest.config.ts
✅ Files skipped from review due to trivial changes (3)
  • docs/query-api-walkthrough.md
  • packages/stack/tests/helpers/stub-protect-ffi-wasm-inline.ts
  • .changeset/eql-v3-typed-schema.md
🚧 Files skipped from review as they are similar to previous changes (4)
  • packages/stack/tests/wasm-inline-column-name.test.ts
  • packages/stack/src/encryption/helpers/infer-index-type.ts
  • packages/stack/package.json
  • packages/stack/src/encryption/operations/encrypt.ts

Comment thread docs/superpowers/plans/2026-07-01-eql-v3-typed-schema.md Outdated
Comment thread packages/stack/__tests__/helpers/eql-v3.ts Outdated
Comment thread packages/stack/__tests__/schema-v3-pg.test.ts
Comment thread packages/stack/src/types.ts
@tobyhede tobyhede force-pushed the feat/eql-v3-text-search-schema branch from b54e6d4 to ed78233 Compare July 1, 2026 03:13
@tobyhede tobyhede changed the title feat(stack): EQL v3 text_search authoring DSL (@cipherstash/stack/schema/v3) feat(stack): EQL v3 typed schema + strongly-typed client (@cipherstash/stack/schema/v3, /v3) Jul 1, 2026
tobyhede added 22 commits July 2, 2026 13:10
Column builders are copied onto the EncryptedTable instance for accessor
access (users.email). A column named build/tableName/columnBuilders/
_columnType would silently overwrite that member — worst case a 'build'
column breaks buildEncryptConfig's tb.build() call at runtime.

Throw a clear error at table-definition time instead. Scoped to v3; v2
retains its existing behavior.

Found by CodeRabbit review.
…ntry

The wasm-inline encrypt entry typed opts.column as the widened structural
BuildableColumn, but getColumnName still gated on instanceof EncryptedColumn
|| EncryptedField and threw for a v3 EncryptedTextSearchColumn — a runtime
break the type promise hid. Resolve the name structurally (typeof getName)
so v3 columns round-trip through WasmEncryptionClient.encrypt(); still throws
for non-builder JS input. getColumnName is the only instanceof gate on this
path; the rest reads table.tableName structurally.

Adds wasm-inline-column-name.test.ts exercising the seam (v2 column/field +
v3 column + non-builder). Like its sibling wasm-inline-normalize.test.ts the
suite cannot load in environments missing the @cipherstash/protect-ffi
/wasm-inline dep subpath.
Config tables are keyed by name, so two tables with the same tableName
silently dropped the earlier one. Add a v3-only additive guard that throws
on a duplicate (Object.hasOwn). v2's buildEncryptConfig keeps its existing
silent-overwrite behavior (no-v2-change constraint).
The RESERVED_TABLE_KEYS guard only covered own members (build, tableName,
columnBuilders, _columnType), so a column named constructor/toString/valueOf/
hasOwnProperty was assigned as an own property, shadowing the Object.prototype
member. Add an `in` check (isReservedTableKey) so any prototype-chain member
is also rejected, keeping the table object well-behaved for reflection.
…freeTextSearch' for match

A v3 text_search column emits unique+ore+match, and shared index inference
picks by priority unique > match > ore. So encryptQuery without an explicit
queryType builds an EQUALITY term (via unique) — a substring matches nothing.
Document on EncryptedTextSearchColumn + encryptedTextSearchColumn that callers
must pass queryType:'freeTextSearch' (FFI 'match') for free-text queries.

Addresses review finding #2 (naming footgun; doc-only, no runtime change).
Add table-driven runtime tests for all 40 EQL v3 domain builders (name,
eqlType, capabilities, config, queryability) plus type-level tests for
nominal domain distinctness, InferPlaintext mapping, queryability of
BuildableQueryColumn, and v3/v2 model inference.
coderdan added 22 commits July 13, 2026 11:51
The gate ran only `fta src/eql/v3` in stack, so relocating the Drizzle v3 code out
to stack-drizzle silently dropped it from coverage — and the v2 adapter monoliths
(supabase query-builder.ts, drizzle operators.ts) were never gated at all.

Adds `analyze:complexity` to both new packages and runs all three from fta-v3.yml
(one step each, so a failure names the package), triggered on the new package
paths. Caps are ratchets at each package's current worst file: stack 69
(columns.ts 68.46), stack-drizzle 89 (operators.ts 88.70), stack-supabase 91
(query-builder.ts 90.41). Lower a cap as a file is refactored below the next
threshold; the two v2 monoliths are the debt to whittle toward stack's 69.

Kept the job name "Analyze v3 complexity" so any required-check config still matches.

Claude-Session: https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w
…apter split

- skills/stash-{drizzle,supabase,encryption}: import from @cipherstash/stack-drizzle
  (+/v3) and @cipherstash/stack-supabase, installed alongside @cipherstash/stack;
  corrected the "included in @cipherstash/stack" framing to separate first-party
  packages.
- docs/reference/supabase-sdk.md + packages/stack/README.md: new package paths;
  README's subpath-export table now lists the adapters as separate packages, not
  stack subpaths.
- AGENTS.md: dropped the three removed subpaths from the export list, added the
  two packages to Repository Layout, updated the integrations section.
- SECURITY.md: added both packages; scoped stack's description.
- examples/basic: import + deps updated.
- Changesets: @cipherstash/stack minor (breaking subpath removal + adapter-kit),
  the two new packages at first release, and a stash patch for the bundled skills.

Historical design docs under docs/superpowers/specs and the separate
@cipherstash/drizzle (protect-based) package are intentionally left unchanged.

Claude-Session: https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w
…r split

Copilot flagged remaining @cipherstash/stack/drizzle references outside the docs:

- CLI `init` scaffolding (utils.ts) generates user Drizzle schema files — now
  imports from @cipherstash/stack-drizzle, not the removed @cipherstash/stack/drizzle
  subpath. `encrypt/context.ts`'s optional runtime import updated to match.
- packages/bench: drizzle bench/test/setup import @cipherstash/stack-drizzle;
  added it as a workspace dependency, and the bench CI job now builds it alongside
  @cipherstash/stack (the db-only smoke doesn't need it, but the full bench does).
- examples/basic already updated in the prior commit.

Claude-Session: https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w
Correctness / packaging:
1. CLI `install-deps` now installs the integration's adapter package
   (@cipherstash/stack-drizzle / -supabase / prisma-next), not just
   @cipherstash/stack — a Drizzle `stash init` was scaffolding an import of a
   package it never installed.
2. Fix the missed rewrite in stack-supabase/integration/helpers/pgrest.ts
   (`@/supabase/types` → `../../src/types`, a deleted path).
3-4. Drop the spurious required `@cipherstash/protect-ffi` peer from both new
   packages (never imported in drizzle; type-only in supabase) — kept as devDep;
   avoids unmet-peer warnings + version conflict on stack's next ffi bump.
5. Remove the now-dead `drizzle-orm`/`@supabase/supabase-js`/`pg` peers +
   peerDependenciesMeta from `@cipherstash/stack` and the inert tsup `external`.
6. Add README.md to both new packages (they list it in `files`).
7. Guard `encryptedSupabaseV3(url, key)`'s dynamic `@supabase/supabase-js` import
   with the same friendly-message remap as the `pg` (`loadPg`) path.

Efficiency / altitude / cleanup:
8. Document that the second per-job integration vitest run's EQL install self-
   skips via `isInstalled` (fast no-op, not a re-apply).
9. Document why test-kit's needle-for imports `matchNeedleError` via `@/` rather
   than the adapter-kit barrel (avoids dragging the encryption/helpers chain into
   test-kit's strict tsc).
10. De-duplicate the vitest configs: a shared `stackSourceAlias` (the `@/` + wasm
   stubs) in vitest.shared, and shared `integrationHarness`/`integrationTestDefaults`
   in @cipherstash/test-kit, so the five near-identical configs stop drifting;
   removed the redundant per-config adapter-kit override + its false comment.

Verified: all three adapter/core packages + cli build (JS+DTS), unit + type tests
green, fta gates pass, code:check + frozen-lockfile clean, integration configs
load (reach requireIntegrationEnv), and full `tsc` over the packages no longer
flags pgrest.ts or the vitest configs.

Claude-Session: https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w
- stack-drizzle/stack-supabase package.json: drop EQL from the descriptions
  (say "searchable field-level encryption"), remove the `eql` keyword, and add
  `encryption`, `searchable-encryption`, and `security` keywords.
- stack-drizzle README: lead with EQL v3 (with an insert/query/decrypt
  money-shot), mark v2 as legacy, and link full docs to
  cipherstash.com/docs/integrations/drizzle.

Claude-Session: https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w
…mple

Reads as encryptedTypes.TextSearch('email') / encryptedTypes.IntegerOrd('age'),
making the schema clearer at a glance (review feedback on #630).

Claude-Session: https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w
feat(stack): adapter package split — foundation + plan (#627)
The query builder's catch block hardcoded `encryptionError: undefined`, so
the typed `EncryptedSupabaseError.encryptionError` field was always empty and
callers could only detect encryption failures indirectly (status/statusText
or `.throwOnError()`).

- Thread the wrapped `EncryptionError` through when the caught error is an
  `EncryptionFailedError`; leave it unset for plain PostgREST/API errors.
- Tighten `EncryptionFailedError.encryptionError` from `unknown` to
  `EncryptionError` (all five throw sites already pass a `result.failure`).
- v3 shares the base `execute()` catch; its `encryptionFailure` helper now
  synthesizes an `EncryptionError` for the contract-violation cases (bulk
  length mismatch, null envelope) that have no underlying failure object.
- Add unit coverage for both dialects: populated on a genuine encryption
  failure, absent on a plain error.

Closes #626

Claude-Session: https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w
…review)

Address self-review findings on the #626 fix:

- Test coverage: the v3 test routed through the shared base `encryptModel`
  path, so the v3-specific `encryptionFailure` synthesize branch this PR adds
  was unasserted (a regression to `cause` would not have failed any test).
  Replace it with a test that drives that branch directly via a bulk
  length-mismatch and asserts `error.encryptionError` is populated. Verified
  by mutation: reverting the branch turns the test red.
- Comment: hedge the base catch-block comment — the wrapped value is the
  operation's `EncryptionError` OR, in v3, a synthesized one.
- Style: collapse the two-line same-module `errors` import into the repo's
  combined `import { type X, Y }` idiom.

Claude-Session: https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w
…review)

Copilot flagged that the v3 synthesize branch had two no-cause call sites
(bulk length mismatch and null envelope) but only the failure-with-cause case
was covered. The length-mismatch path is now tested; add the null-envelope
path too so both no-cause call sites explicitly pin `error.encryptionError`.

Claude-Session: https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w
…rror

fix(supabase): populate EncryptedSupabaseError.encryptionError (#626)
… encrypt (#622)

Every v3 Drizzle operator (eq/ne/gt/gte/lt/lte/between/notBetween/inArray/
notInArray/contains) previously encrypted its operand with client.encrypt — a
full storage envelope (with ciphertext `c`) cast to `::jsonb`. A WHERE operand
should be a query term, not a value to store.

- All operators now use client.encryptQuery, producing a ciphertext-free term
  cast to the column's `eql_v3.query_<domain>` type, reaching the bundle's
  `(domain, query_<domain>)` overloads. The dialect no longer adds `::jsonb`;
  the operator layer owns the query-domain cast (`queryCastForDomain`).
- encryptQuery is now a REQUIRED capability of the operand client (the optional
  guard and its only-JSON-uses-it branch are gone); scalar and JSON paths are
  unified.
- inArray/notInArray encrypt the whole list in one batch encryptQuery crossing
  (position-stable, wrong-length rejected), replacing the bulkEncrypt/
  bounded-concurrency path.
- stack: add a batch `encryptQuery(terms)` overload to TypedEncryptionClient
  (mirrors the nominal client) so the documented
  `createEncryptionOperatorsV3(await EncryptionV3(...))` usage type-checks with
  the batch path.
- Verified the query-domain mapping and the composite (hm/op/bf) query-term
  shape against the eql-3.0.0 bundle and a live encryptQuery probe; unit tests
  updated to assert query-term operands and `::eql_v3.query_<domain>` casts. The
  end-to-end operator behaviour is covered by the Drizzle integration matrix.

Closes #622

Claude-Session: https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w
…med overload

Replace the object-literal arrow + `as unknown as` bridge with a named function
carrying the two real overload signatures (single per-column form + batch
`terms[]` form). The compiler now checks the implementation against both
signatures directly, so the whole-value cast is gone; only the forwarded args
keep `as never`, consistent with the sibling encrypt/encryptModel wrappers.

Pure type-level restructuring — the runtime forward to the nominal client is
unchanged. Verified: stack + stack-drizzle build and test:types (incl. the M1
guard) pass, code:check clean.

Claude-Session: https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w
- operators.ts: drop the two dead regex branches in queryCastForDomain
  (`ord_ope`, `search_ore` are not real column-domain suffixes — ope ordering
  is `_ord`, text search is `_search`); comment now ties the suffixes to the
  column factories.
- operators.ts: fix stale docs left by the bulkEncrypt→encryptQuery move — the
  inArray/notInArray JSDoc and the inArrayOp comment no longer describe the
  removed bulkEncrypt path / deleted concurrency fallback.
- operators.test.ts: the encryptQuery double now routes to batch only when
  `opts === undefined && Array.isArray(...)`, matching the real client — an
  array-valued single query WITH opts (e.g. a searchableJson array needle) no
  longer diverges to the batch path in the double.
- stash-drizzle skill: correct the inArray/notInArray line (single encryptQuery
  batch crossing, not bulkEncrypt); + stash patch changeset (skills ship in the
  tarball).

Claude-Session: https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w
…query

feat(stack-drizzle): encrypt v3 query operands with encryptQuery (#622)
`@cipherstash/stack/wasm-inline` created a client pinned to the FFI's EQL v2
wire format, so v3 schemas failed every encrypt on the edge (Deno / Bun /
Cloudflare Workers / Supabase Edge). Make the entry EQL v3:

- construct the WASM client with `eqlVersion: 3`
- re-export the v3 authoring surface (`types`, `encryptedTable`, the column
  classes, `buildEncryptConfig`, inference helpers) so an Edge Function
  authors and runs v3 from one import
- accept only v3 tables; reject a v2 table with a clear error

The v2 WASM path was never announced, documented, or used; EQL v2 remains
fully supported on the native `@cipherstash/stack` entry.

Closes #614
…tches() (#617)

EQL v3 encrypted free-text search is fuzzy bloom-filter token matching — order-
and multiplicity-insensitive, one-sided (a `true` may be a false positive) — not
containment. `contains()` promised substring/containment semantics it never had.
Rename it to `matches()` on the encrypted surface; keep `contains()` for genuine
exact containment.

Drizzle (`/v3`):
- `matches()` = bloom free-text on text_match/text_search columns.
- `contains()` = exact encrypted-JSON `@>` on `types.Json` (ste_vec) columns.
- Split gating: matches requires free-text; contains requires JSON containment.

Supabase:
- `.matches()` = encrypted free-text (new `'matches'` FilterOp, additive to the
  shared base builder so v2 is untouched; rides the existing `cs`/`@>` plumbing).
- `.contains()` = native jsonb/array `@>` on plaintext columns; throws on an
  encrypted column, pointing to matches().
- Typed surface: `matches` is keyed on encrypted-free-text-only columns
  (`V3EncryptedFreeTextKeys`) so `matches()` on a plaintext column is a COMPILE
  error, not a runtime throw; `contains` is keyed on plaintext columns. The
  runtime throws are the untyped-path backstop.
- `like()`/`ilike()` on an encrypted column are no longer rejected — they delegate
  to matches() as a best-effort compatibility shim (approximate: fuzzy, case-
  insensitive, one-sided; surrounding `%` stripped, internal `%`/`_` rejected, a
  one-time warning). Plaintext columns keep real SQL LIKE. The typed surface still
  hides like/ilike (steering to matches at compile time); delegation serves the
  untyped/JS path.

Docs + skills (stash-drizzle, stash-supabase) updated; changesets added (both
adapters minor, stash patch for skills). Follow-ups: eql-repo rename
(encrypt-query-language#396) and the delegating-facade refactor (#638).

Closes #617

Claude-Session: https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w
#617)

CI on #640 caught that the integration matrices (separate from the unit
__tests__/) still called the old encrypted contains(). Complete the rename
there:

- test-kit: the abstract matrix op `contains` → `matches` (free-text only;
  searchableJson stays deferred to the dedicated json-contains suite); oracle
  `containsPlain` → `matchesPlain`; run-family-suite + index updated.
- Drizzle/Supabase integration adapters: `supportedOps` + `run()` case
  `contains` → `matches` (encrypted free-text).
- Drizzle relational suite: free-text `ops.contains` → `ops.matches`; the
  json-contains suite keeps `ops.contains` (exact JSON @>).
- Supabase wire suite: encrypted `.contains('email')` → `.matches('email')`;
  plaintext `.contains('tags'/'meta')` + or-conditions unchanged; the old
  "refuses like()" test replaced with delegation coverage (`like('%ada%')`
  delegates to matches; an internal `%`/`_` throws).

Local: builds, unit + type tests, code:check all green; integration verified by
CI.

Claude-Session: https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w
feat(stack): make the wasm-inline entry EQL v3 (#614)
…640)

- Narrow the untyped v3 `matches()` operand from NativeContainsValue to `string`:
  the free-text term is always a string, so `matches('email', {a:1})` is now a
  compile error on the untyped surface too, not just a runtime throw.
- Close the last `contains`-name bypass: `not(col, 'contains', …)` on an
  encrypted column rewrote `contains`→`cs` in the base not() path, letting fuzzy
  bloom matching run under the `contains` name. The v3 builder now rejects it and
  steers to the honest `matches` spelling. (`.filter(col,'contains',…)` and
  structured `.or([{op:'contains'}])` already throw via queryTypeForRawOp; only
  not() rewrote it.) Updated the two wire tests to the `matches` spelling and
  added a rejection test.

Claude-Session: https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w
…to-matches

feat(stack)!: rename EQL v3 encrypted contains() → matches() (#617)
…h-recs

feat(cli): default EQL to v3, stop recommending db push (v2/Proxy-only)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants