Skip to content

feat(stack,cli): EQL v3 bigint + Supabase v3 + CLI install path, re-expressed on protect-ffi 0.28#583

Open
tobyhede wants to merge 5 commits into
chore/eql-v3-ffi-0-28from
feat/eql-v3-bigint-restack
Open

feat(stack,cli): EQL v3 bigint + Supabase v3 + CLI install path, re-expressed on protect-ffi 0.28#583
tobyhede wants to merge 5 commits into
chore/eql-v3-ffi-0-28from
feat/eql-v3-bigint-restack

Conversation

@tobyhede

@tobyhede tobyhede commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Re-expresses the net-new work from origin/james/cip-3291-bigint-stack (protect-ffi 0.27, old eql_v3.* naming) onto this base's newer EQL v3 surface: public.* type domains, schema-driven cast_as routing, and protect-ffi 0.28 (native JS bigint round-trip). Because the branches diverged on the domain surface, each feature is re-derived against BASE conventions rather than cherry-picked. Stacked on chore/eql-v3-ffi-0-28 (PR #575) — set that as the merge base, not main.

Commits (one per feature)

  1. fix(stack) hyphen fixfieldsForModelIndex() splits bulk-model result ids at the first hyphen only, so field keys containing - survive bulkEncrypt/DecryptModels. Regression test fails against the old naive split('-').
  2. feat(stack) bigint domain family (un-gated)types.Bigint* + EncryptedBigint*Column over the public.bigint* domains, bigint PlaintextKind/Plaintext arm. Shipped un-gated: protect-ffi 0.28 marshals a native JS bigint losslessly (bigintWire: encode on encrypt, JsBigInt::from_i64 on decrypt), so no reconstruction is needed and the live matrix exercises the round-trip. Removes the base's skip-v3-bigint changeset and flips the @ts-expect-error guard to a positive assertion.
  3. feat(cli) EQL v3 install path + db installeql install--eql-version <2|3> installs vendored public.* bundles (derived from the stack fixture via scripts/build-eql-v3-sql.mjs), keyed to the eql_v3 schema; v3 is direct-install-only. db install becomes a deprecated alias. (Scoped to the install rename — the eqlVersion runtime client flag is intentionally not ported; BASE supersedes it with cast_as routing.)
  4. docs — reconcile living docs (stash-cli skill, migrate README) to stash eql install + document --eql-version.
  5. feat(stack) encryptedSupabaseV3 — EQL v3 dialect of the Supabase adapter. EncryptedQueryBuilderImpl gains protected seams (v2-preserving defaults); a v3 subclass overrides them for property↔DB resolution, raw jsonb mutations, full-envelope filter operands, like/ilikecs, Date reconstruction, and capability validation.

Verification

  • pnpm --filter @cipherstash/stack test:types → 67 ✓ (the Record<EqlV3TypeName, DomainSpec> catalog gate covers the new public.bigint* rows)
  • Full stack suite: 516 passed; the 18 failures are the pre-existing Client key not configured environmental baseline (unchanged with/without this branch, confirmed by stashing)
  • CLI 330 ✓, wizard 140 ✓, migrate 19 ✓
  • Pending live-PG: matrix-live-pg / schema-v3-pg prove the bigint round-trip end-to-end but need CS workspace creds (CS_*) — run in CI or locally with creds. Code is verified against 0.28's bigintWire FFI contract.

tobyhede added 5 commits July 8, 2026 16:02
The bulk model helpers key per-field operation results by
`${modelIndex}-${fieldKey}` ids and reconstructed models with a naive
key.split('-'), truncating any field key containing a hyphen
(some-field -> some): the value landed under the truncated key and the
real field silently vanished. Duplicated across all four multi-model
reconstruction sites.

Extract one fieldsForModelIndex helper that splits at the FIRST hyphen
only and use it at all four sites. Offline regression tests (mocked
protect-ffi) prove hyphenated and multi-hyphen field names survive
bulkEncryptModels / bulkDecryptModels -- both fail against the naive
split.

Re-expressed from james/cip-3291-bigint-stack 632bdf6.
…n protect-ffi 0.28)

Add BIGINT/BIGINT_EQ/BIGINT_ORD_ORE/BIGINT_ORD domain definitions against
the public.bigint* concrete domains, the EncryptedBigint*Column classes,
types.Bigint* factories, barrel exports, and union membership, following the
existing per-domain pattern. The SDK-wide Plaintext union gains bigint, and
PlaintextKind/PlaintextFromKind map cast_as 'bigint' to the JS bigint type
(bigint columns always decrypt to a JS bigint).

Shipped UN-GATED: protect-ffi 0.28 marshals a native JS bigint across the Neon
boundary losslessly (encode on the way in, JsBigInt::from_i64 on decrypt), so
no reconstruction is needed on the decrypt path and the live matrix suites
exercise the round-trip directly. i64 bounds are enforced at the protect-ffi
boundary. Index emission follows the numeric rule: bigint_eq -> unique (hm);
bigint_ord/bigint_ord_ore -> ore (equality via ob).

Removes the base's bigint skip changeset and flips the schema-v3 type guard
from a negative (@ts-expect-error) to a positive assertion. The catalog-driven
capability sweep and live-PG matrix pick up the four public.bigint* rows
automatically (no liveGate).

Re-expressed from james/cip-3291-bigint-stack 2495c65 + e593415 against the
public.* domain surface and protect-ffi 0.28.
… eql install

Vendor the v3 SQL bundles into packages/cli/src/sql via a checked-in
derivation script (scripts/build-eql-v3-sql.mjs): the full bundle is a
byte-identical copy of the stack public.* fixture monolith; the Supabase
variant strips the two CREATE OPERATOR CLASS/FAMILY chunks at their --! @file
markers (they need superuser).

EQLInstaller gains an eqlVersion option on install/isInstalled/
getInstalledVersion; v3 keys install-detection, version(), and Supabase grants
to the eql_v3 schema (type domains live in public, operators in eql_v3,
constructors in eql_v3_internal). v3 + --latest is rejected (no public
artifacts). eql install --eql-version 3 is direct-install only —
--drizzle/--migration/--migrations-dir/--latest are rejected up front and
auto-detected drizzle falls back to direct with a notice.

Move db install under a dedicated eql command group as eql install; db install
stays as a deprecated alias that warns and forwards. Help text, next-step
hints, generated Supabase migration headers, wizard allowlist + steps, docs,
and tests all reference eql install.

Re-expressed from james/cip-3291-bigint-stack d15414a + e027b74 + 64fdeb2
against the public.* v3 bundle (scoped to the install rename; the eqlVersion
runtime client flag and eql upgrade/status moves are intentionally not ported).
…ent --eql-version

Update the stash-cli skill and @cipherstash/migrate README to the renamed
`stash eql install` command (keeping `stash db install` documented as the
deprecated alias), and document the new `--eql-version <2|3>` install flag and
its v3 direct-install-only constraints in the skill.

Re-expressed from james/cip-3291-bigint-stack 64fdeb2/d15414a5 docs. Historical
CHANGELOGs and dated design docs are left as point-in-time records; the base
already migrated the domain surface to public.*.
…pter

The v2 query mechanism (direct EQL operators over PostgREST) is unchanged:
EncryptedQueryBuilderImpl gains narrow protected seams whose defaults preserve
the v2 behaviour byte-for-byte, and a v3 subclass (query-builder-v3.ts)
overrides them for native public.* concrete-domain columns:

- column recognition + property<->DB name resolution via buildColumnKeyMap
  (filters, mutations, aliased select casts prop:db::jsonb)
- raw jsonb mutation payloads (no eql_v2 composite wrap)
- full-envelope filter operands: every public.* domain CHECK requires the
  storage keys (v/i/c + index terms) and the SQL operators coerce their jsonb
  operand into the domain, so a narrowed encryptQuery term fails 23514 on EVERY
  domain — all operands go through encrypt() instead
- like/ilike on encrypted columns -> PostgREST cs (bloom @>)
- Date reconstruction from cast_as (date/timestamp) on decrypted rows
- capability validation: filters on storage-only columns or unsupported query
  types throw typed + runtime errors; filter keys are type-narrowed

Wire-encoding unit tests (mock encryption + supabase clients) cover both
dialects, including v2 regression pins for the seams.

Re-expressed from james/cip-3291-bigint-stack ecd3f38 against the base's
query-builder and the public.* domain surface (protect-ffi 0.28).
@tobyhede tobyhede requested a review from a team as a code owner July 8, 2026 09:33
@changeset-bot

changeset-bot Bot commented Jul 8, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: f2d38d8

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

This PR includes changesets to release 8 packages
Name Type
@cipherstash/stack Minor
stash Minor
@cipherstash/wizard Minor
@cipherstash/bench Patch
@cipherstash/prisma-next Patch
@cipherstash/basic-example Patch
@cipherstash/prisma-next-example Patch
@cipherstash/e2e 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 Jul 8, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 28be72e0-902a-4dfc-a0f7-0ccb5fce3b57

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:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/eql-v3-bigint-restack

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 on lines +122 to +130
// v3 supports the direct install path only. Explicit --drizzle/--migration
// are rejected up-front by validateInstallFlags; auto-DETECTED drizzle or
// migration modes fall back to direct here rather than erroring.
if (eqlVersion === 3 && resolved.drizzle) {
p.log.info(
'EQL v3 does not support the Drizzle migration path yet — installing directly.',
)
resolved.drizzle = false
}

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.

Is there an issue tracked for this?

Comment on lines +53 to +55
/** The v3 (`eql_v3`) Supabase grants block. See {@link supabasePermissionsSql}. */
export const SUPABASE_PERMISSIONS_SQL_V3 =
supabasePermissionsSql(EQL_V3_SCHEMA_NAME)

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.

Clarification: the EQLV3 install now gracefully degrades to OPE if no operator class/family support is available. Is the dedicated Supabase installer needed for V3 now?

Comment on lines +287 to +293
if (latest && eqlVersion === 3) {
// No public v3 release artifacts exist yet — the v3 bundles are vendored
// from the generated monolith (see scripts/build-eql-v3-sql.mjs).
throw new Error(
'`--latest` is not supported for EQL v3 yet: no public v3 release artifacts exist. Use the bundled install.',
)
}

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.

Captured somewhere?

```

State is tracked in an append-only `cipherstash.cs_migrations` table installed by `stash db install`. The EQL intent (which indexes, which cast_as) continues to live in `eql_v2_configuration` so Proxy continues to work against the same database.
State is tracked in an append-only `cipherstash.cs_migrations` table installed by `stash eql install`. The EQL intent (which indexes, which cast_as) continues to live in `eql_v2_configuration` so Proxy continues to work against the same database.

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.

This might need to be updated to differentiate between V2 and V3.

Comment on lines +85 to +91
* const users = encryptedTable('users', {
* email: types.TextSearch('email'), // public.text_search
* amount: types.IntegerOrd('amount'), // public.integer_ord
* })
*
* const client = await Encryption({ schemas: [users] })
* const es = encryptedSupabaseV3({ encryptionClient: client, supabaseClient: supabase })

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.

This interface differs to what is currently on the launch blog post. I think a Rails-style auto-config loading from the schema is the money shot of V3. I'd love for us to be able to make it happen if we can. If not, we'll need to update the blog post.

@coderdan coderdan 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.

This is looking really solid. A couple of things I'd like to align on before explicitly approving, but the direction is really good.

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.

2 participants