Skip to content

feat(stack): Bigint domains + Supabase support (i64-bounded) — CIP-3291 [gated on protect-ffi release]#557

Draft
freshtonic wants to merge 90 commits into
feat/eql-v3-text-search-schemafrom
james/cip-3291-bigint-stack
Draft

feat(stack): Bigint domains + Supabase support (i64-bounded) — CIP-3291 [gated on protect-ffi release]#557
freshtonic wants to merge 90 commits into
feat/eql-v3-text-search-schemafrom
james/cip-3291-bigint-stack

Conversation

@freshtonic

Copy link
Copy Markdown
Contributor

Summary

Adds Bigint support to the stack SDK and its Supabase adapter — the Bigint work stream of CIP-3291. This is step 3 of the CIP-3291 merge order. It was cut from #547's branch (james/cip-3300-spike-integrate-eql-v3-into-supabase-orm, the full EQL v3 surface); #547 has since merged to main (merge commit 93b29291 is main's tip), so this PR targets main directly and its diff is exactly the bigint work.

Recorded decisions (from the Linear ticket — not relitigated here)

  • Bounds = full i64 (-2^63 … 2^63 - 1). Enforcement lives at the protect-ffi boundary (a parallel PR adds JS BigInt to protect-ffi's JsPlaintext on the eql_v3 release line). The stack layer's job is types + domains + adapters + surfacing those errors — out-of-range values surface as encryption errors, never silent truncation.
  • bigint columns decrypt to JS bigint, always (never a precision-lossy number).
  • Scope excludes *_ord_ope (CIP-3403): this adds bigint, bigint_eq, bigint_ord, bigint_ord_ore — the same families every other int has.

🚧 Why this is a DRAFT — the gate

The pinned @cipherstash/protect-ffi 0.27.0 does not accept/return JS bigint (JsPlaintext lacks it): encrypting a bigint throws at runtime. The pin is deliberately NOT bumped here. This PR un-drafts when ALL of:

  1. the protect-ffi release with JS BigInt in JsPlaintext ships (parallel FFI PR on the eql_v3 release line);
  2. the protect-ffi pin is bumped in this repo;
  3. live-test verification passes: remove liveGate from the four bigint rows in __tests__/v3-matrix/catalog.ts — the live matrix suites (matrix-live, matrix-live-pg) then automatically pick the bigint domains up (mega-table round-trips at i64::MAX / i64::MIN / 0n / a >2^53 mid value, plus the pg eq/ord proofs) — and run green against live creds + a live DB.

Until then, all runtime coverage is mock-based (the repo's established mock pattern in supabase-v3-builder.test.ts), and the live suites emit explicit, reason-bearing skips for the bigint rows instead of silently omitting them.

What changed

SDK (packages/stack/src):

  • eql/v3/columns.ts: BIGINT/BIGINT_EQ/BIGINT_ORD_ORE/BIGINT_ORD domain consts (eql_v3.bigint*, castAs: 'bigint'), EncryptedBigint*Column classes, AnyEncryptedV3Column membership, PlaintextKind/PlaintextFromKind gain 'bigint'bigint.
  • eql/v3/types.ts: types.Bigint / BigintEq / BigintOrdOre / BigintOrd factories (naming convention preserved); barrel exports in eql/v3/index.ts.
  • types.ts: Plaintext = JsPlaintext | Date | bigint, with the gate documented.
  • Index emission needed no new code: the capability presets already produce the non-text rule — bigint_equnique (hm); bigint_ord/bigint_ord_oreore only (equality answered via ob, like integer_ord). Pinned by regression tests.
  • schema/index.ts already had 'bigint''big_int' in castAsEnum/toEqlCastAs (and the wasm normalizeCastAs path) — no change needed.

Supabase adapter: no runtime code change needed — query-builder-v3.ts is value-generic (filter operands go through encrypt(); only post-encryption envelopes are JSON-encoded, and envelopes never contain a raw bigint). Coverage added instead (below).

Decrypt-path bigint survival (verified): the model decrypt path (encryption/helpers/model-helpers.ts → FFI decryptBulk) and the Supabase decryptResultsdecryptModel/bulkDecryptModelspostprocessDecryptedRow path pass FFI-returned values through with no JSON round-trip — a JS bigint returned through the Neon boundary reaches the caller intact. rowReconstructor needs no bigint arm (comment updated). The only JSON.stringify on the query path operates on post-encryption envelopes (safe).

Tests:

  • v3-matrix/catalog.ts: 4 bigint rows (compile-time coverage gate satisfied); BIGINT_S samples include 9223372036854775807n (i64::MAX), -9223372036854775808n (i64::MIN), 0n, and 4611686018427387904n (> 2^53, proves losslessness); new DomainSpec.liveGate field carries the skip reason naming the gate.
  • matrix-live / matrix-live-pg: live-gated domains are excluded from the shared mega tables / seed INSERT / term encryption — a bigint through FFI 0.27 (including the v2-wire term client) throws at runtime in beforeAll, which would take every other domain's live proof down — and reported as explicit skips.
  • matrix.test-d.ts: bigint_ord infers plaintext bigint + 'equality' | 'orderAndRange'.
  • schema-v3.test.ts: bigint joins the equality-via-ORE and ore-only (no unique) regression lists.
  • supabase-v3-builder.test.ts (mock, wire-encoding): types.BigintOrd column — insert delivers the real bigint to encrypt; eq/gte operands JSON-encode safely as full envelopes; decrypted rows return a JS bigint through decryptResults.
  • supabase-v3.test-d.ts: bigint column plaintext type is bigint; filter/insert value types pin to bigint (number/string rejected).

Docs: docs/reference/supabase-sdk.md + skills/stash-supabase/SKILL.md gain the bigint domains (types/DDL examples), the i64 bounds + boundary-error semantics, and the gate note. Changeset added (minor).

Test evidence

  • pnpm exec tsc --noEmit (packages/stack): 198 errors — byte-identical to the pre-change baseline (all pre-existing in __tests__; src/ at 0). Verified by diffing baseline vs after.
  • pnpm exec vitest run — v3-matrix deterministic suites (matrix now 39 domains), schema-v3, schema-builders, supabase-v3-builder (27 tests incl. 3 new bigint cases), typed-client-v3, schema-v3-client: all green (live suites skip locally as normal).
  • pnpm exec vitest --run --typecheck.only: 7 files, 66 tests, no type errors (incl. the new bigint type assertions).
  • biome on touched files: clean (one pre-existing unused-import warning on the base, unchanged).

Merge order

Step 3 of CIP-3291: (1) protect-ffi JS BigInt PR → (2) base stack PR #547 (merged) → (3) this PR → then pin bump + liveGate removal + live verification un-drafts it.

dependabot Bot and others added 30 commits July 3, 2026 05:20
Bumps [hono](https://github.com/honojs/hono) from 4.12.19 to 4.12.27.
- [Release notes](https://github.com/honojs/hono/releases)
- [Commits](honojs/hono@v4.12.19...v4.12.27)

---
updated-dependencies:
- dependency-name: hono
  dependency-version: 4.12.25
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
…-4.12.25

chore(deps): bump hono from 4.12.19 to 4.12.27
Bumps [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) from 3.2.4 to 3.2.6.
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md)
- [Commits](https://github.com/vitest-dev/vitest/commits/v3.2.6/packages/vitest)

---
updated-dependencies:
- dependency-name: vitest
  dependency-version: 3.2.6
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
…st-3.2.6

chore(deps): bump vitest from 3.2.4 to 3.2.6
The install command scaffolds stash.config.ts and installs the EQL
extensions, so it now lives under a dedicated `eql` command group.
`stash db install` keeps working as a deprecated alias that prints a
warning pointing at the new name.

- Add `eql` command group to the CLI with `install` as its first
  subcommand; `db install` forwards to the same implementation after a
  deprecation warning.
- Update help text, next-step hints, error messages, and the generated
  Supabase migration header to reference `stash eql install`.
- Allow `stash eql` in the wizard agent command allowlist (alongside the
  still-allowed `stash db`) and shell out to `stash eql install` in the
  wizard's post-agent steps and prerequisites hint.
- Update READMEs, the stash-cli skill, agent doctrine, and all tests;
  add smoke coverage for the new group and the deprecated alias.
`upgrade` and `status` manage the EQL extension itself, so they move to
`stash eql upgrade` / `stash eql status` alongside `eql install`. The
old `db` spellings keep working as deprecated aliases that print a
warning, matching the `db install` treatment. Generalise the deprecation
message factory, update help text, intros, setup-prompt references,
README, skill, doctrine, and tests.
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 since f1248d5.

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.

Flagged in the consolidated review on #547 (pre-existing, out of that
PR's scope).
…on + server-side lock-context note

Two additions flagged by the consolidated review on #547:

- an explicit region→workspaceCrn migration paragraph for the
  WASM-inline path (what to set, where the CRN comes from, that region
  is now ignored)
- a paragraph spelling out that lock-context enforcement is now
  server-side only: a wrong/missing identity claim surfaces as a
  ZeroKMS decryption failure rather than a client-side throw
fix(stack): bulk-model id split corrupts field names containing hyphens
…et-migration-notes

docs(stack): protect-ffi 0.26 changeset — region→workspaceCrn migration + server-side lock-context enforcement note
…h-db-install-command-to-npx-stash-eql-install

feat(cli): rename `stash db install` to `stash eql install`
- SECURITY.md: replace stale per-package version tables with a
  drift-proof support policy + current package list (adds stash CLI,
  migrate, prisma-next, wizard); fix scope (repo name, npm namespace);
  document the full supply-chain control set; fix typo
- AGENTS.md: correct repo name, add missing packages (cli, wizard,
  migrate, prisma-next, bench), fix examples list, replace dead docs/*
  links with cipherstash.com/docs, add stash-cli skill, complete
  subpath export list
- CONTRIBUTE.md: rewrite around @cipherstash/stack and cipherstash/stack
  (was still @cipherstash/protect + protectjs URLs + JSEQL naming)
- package.json: rename root to @cipherstash/stack-monorepo, mark
  private, fix bugs/repository URLs
- Delete .cursorrules (documented the pre-rename API; AGENTS.md is the
  single source for agent guidance now)
- .cursor/commands/create-example-app.md: rebrand to stack, current
  API names and real CS_* env vars
- docs-feedback issue template: point CoC link at cipherstash/stack
- All READMEs: repository/license/star-badge URLs now point at
  cipherstash/stack
- protect: replace 14 dead in-repo docs links with cipherstash.com/docs
  URLs (the docs/ tree was removed in def9f4b); pin the architecture
  diagram to a permalink of the last commit that contained it; add a
  tip pointing new projects at @cipherstash/stack
- nextjs: rewrite README — it described the package as
  '@cipherstash/protect ... the main package'
- schema, drizzle, protect-dynamodb: retitle away from Protect.js
  branding and add tips pointing at the @cipherstash/stack equivalents
  (encryptedTable/encryptedColumn, stack/drizzle, encryptedDynamoDB)
- changeset: patch bumps so the corrected READMEs ship to npm
- AGENTS.md: bundling guidance was out of date — the WASM entry
  (@cipherstash/stack/wasm-inline) is designed to be bundled and is the
  answer for edge/serverless runtimes; native-FFI externalization only
  applies to the default entry
- SECURITY.md: release.yml uses OIDC trusted publishing with NO
  NPM_TOKEN — previous text claimed the opposite
- create-example-app.md: prefer 'npx stash auth login' profile auth for
  local dev; CS_* env vars are for CI/deployment
- Replace hello@cipherstash.com with humans@cipherstash.com across the
  repo (root + 9 package.json author fields)
- CONTRIBUTE.md: tag repo-tree fence as text (markdownlint MD040)
chore: refresh root meta files after protectjs → stack rename
docs: refresh published package READMEs after protectjs → stack rename
tobyhede and others added 26 commits July 4, 2026 20:47
The structural builder contracts (BuildableColumn, BuildableQueryColumn,
BuildableV3QueryableColumn, BuildableTable, BuildableTableColumns) and the
encryptModel/bulkEncryptModels return-type mapper (EncryptedFromBuildableTable)
appear in public return positions but were not re-exported from
`@cipherstash/stack/types`, so consumers could not name them — an inconsistency
with the already-exposed `EncryptedFromSchema`. No build breakage (the mapped
types were emitted inline); this closes the nameability gap.

Regression guard: types-public-surface.test-d.ts imports each contract from the
public `@/types-public` entrypoint (a missing re-export fails typecheck).

Note: these types are inherited from the base branch (feat/eql-v3-text-search-schema,
PR #535); the export is added here in response to review feedback on the stacked PR.
The v3-matrix domain suite (catalog.ts + matrix tests) landed on the base
branch via PR #540 after this branch was cut, and used the pre-refactor
`@/schema/v3` path and `encrypted<Domain>Column` factories. Retarget it to
`@/eql/v3` and the `types.*` namespace so the base's matrix coverage keeps
working on top of the refactor. `EqlTypeForColumn` (which #540's catalog.ts
consumes) is preserved — ported into eql/v3/columns.ts and re-exported from the
barrel during the rebase.

Post-rebase reconciliation only; no behavior change.
Close two coverage gaps on the eql/v3 branch that only live/e2e tests
touched:

- encrypt-lock-context-guards: assert NaN/+Inf/-Inf are rejected on the
  `encrypt(...).withLockContext(...)` path and short-circuit before the
  FFI call. The non-lock guards run only under the live number-protect
  suite; the lock-context arm (encrypt.ts:163-168) had no coverage.
- wasm-inline-new-client: assert the protect-ffi 0.25 single-object
  `newClient({ strategy, encryptConfig, clientId, clientKey })` shape,
  incl. cast_as normalisation. Previously exercised only by the
  secret-gated Deno e2e, so a regression to the 0.24 two-arg form would
  pass normal CI.

Both run offline (mocked FFI).
The all-35-domain live Postgres suite was force-skipped (describe.skip, not
credential-gated) after `beforeAll`'s dynamic INSERT crashed with
`invalid input syntax for type json` (PR #540). That crash was a postgres.js
serialization gap — a bare ciphertext object stringified to "[object Object]" —
and was fixed 32 minutes later by wrapping every INSERT param in `sql.json(...)`
(commit 53cf854). The force-skip was simply left stale; it is not an FFI
limitation.

Restore the credential-gated form (`LIVE_EQL_V3_PG_ENABLED ? describe :
describe.skip`) as the file's own comment instructed, so the 35-domain SQL
round-trip runs in CI (which supplies DATABASE_URL + CS_* creds) and self-skips
locally. The genuine FFI-level skip — timestamptz `cast_as:'date'` time-of-day
truncation in schema-v3-client.test.ts — stays skipped (needs a native
'timestamp' cast_as variant). timestamptz matrix cases are unaffected (midnight
samples, no truncation).
…equirement)

The `eql_v3.text_ord` and `eql_v3.text_ord_ore` Postgres domains require BOTH
`hm` (HMAC) and `ob` (ORE) in the stored ciphertext — text equality is
HMAC-based (their `eql_v3.eq_term` extracts `hm`), unlike numeric/date order
domains which answer equality via `ob` and need only ORE. The SDK's
`indexesForCapabilities` treated every order/range domain identically, emitting
`ore` only, so text-order ciphertexts lacked `hm` and a real INSERT failed with
`value for domain eql_v3.text_ord_ore violates check constraint`. (Surfaced by
re-enabling matrix-live-pg; masked before by the suite skip.)

Make index derivation castAs-aware: emit `unique` (hm) when equality is
answered via HMAC — equality-only domains of any type, AND text order domains
(`string` + order/range). Numeric/date order domains are unchanged (`ore` only).

Query path follows automatically: `resolvesEqualityViaOre` only fires when
`unique` is absent, so text-order equality now resolves to the `hm` index
(eq_term) while numeric/date order equality still resolves to `ore`.

TDD: text_ord/text_ord_ore build() now emits { unique, ore }; numeric order
stays { ore }; text-order equality resolves to unique. Catalog + matrix build()
assertions updated (TEXT_ORD_IDX). Verified against the eql_v3 domain checks in
the fixture; live SQL runs in CI.
… guards

Test-only additions (separated from the in-flight EQL v3 bundle upgrade so they
land on this branch, not the bundle branch):

- encrypt-lock-context-guards.test.ts: run every non-finite-number guard case
  against BOTH a v2 fluent-builder column and a v3 domain column, since the
  guard lives on the shared EncryptOperationWithLockContext.
- schema-v3.test.ts: `.freeTextSearch()` no-arg is a no-op (pins the
  opts===undefined branch); a text_match mutable-state aliasing guard (base-class
  match-clone path, which the text_search-only test can't cover); and
  buildEncryptConfig() with zero tables yields { v: 1, tables: {} }.
- wasm-inline-strategy.test.ts: Biome line-wrap formatting only.
- encryption/v3: reconstructRow → rowReconstructor factory — the table
  config (build() + buildColumnKeyMap()) is row-invariant but was
  rebuilt per row on the bulk decrypt path; it is now derived once per
  call site, with date columns resolved up front
- encrypt operations: replace the two inline NaN/Infinity guard copies
  with the existing assertValidNumericValue helper (validation.ts)
- schema/match-defaults: single source of truth for the default match
  index parameters (previously duplicated between the v2 freeTextSearch
  builder and the v3 domain builders) plus a shared cloneMatchOpts
  deep-clone used at all three v3 clone sites
- tests: one shared live-gate helper (LIVE_CIPHERSTASH_ENABLED /
  LIVE_EQL_V3_PG_ENABLED + describeLive/describeLivePg) replaces the
  gate blocks copy-pasted across seven live suites

No behavioral changes: emitted encrypt configs are byte-identical
(schema-v3 fixture tests unchanged), guard error messages unchanged,
gating semantics unchanged.
…pter

The v2 query mechanism (direct EQL operators over PostgREST) unchanged;
EncryptedQueryBuilderImpl gains narrow protected seams whose defaults
preserve the v2 behaviour byte-for-byte, and a v3 subclass overrides them:

- 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 eql_v3.* 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 (c?: never)
  fails 23514 on EVERY domain — not just text_search as the design spec
  assumed. All operands go through encrypt() instead.
- like/ilike on encrypted columns → PostgREST cs (bloom @>); the domains
  define no LIKE operator
- Date reconstruction from cast_as on decrypted rows
- capability validation: filters on storage-only columns or unsupported
  query types throw typed + runtime errors

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

Part of CIP-3300; design spec in PR #546.
- 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 fixture monolith; the Supabase variant
  strips the two CREATE OPERATOR CLASS/FAMILY chunks at their --! @file
  markers, mirroring upstream's **/*operator_class.sql exclusion glob.
  Temporary vendoring (sync risk documented) until upstream ships v3
  release artifacts.
- EQLInstaller: eqlVersion option on install/isInstalled/
  getInstalledVersion; v3 + --latest rejected (no public artifacts);
  grants keyed to the installed schema via the new
  supabasePermissionsSql(schemaName) helper (SUPABASE_PERMISSIONS_SQL
  unchanged, SUPABASE_PERMISSIONS_SQL_V3 added).
- stash db install --eql-version 3: direct install only for now —
  explicit --drizzle/--migration/--latest are rejected up-front,
  auto-detected drizzle falls back to direct with a notice.

Part of CIP-3300.
- supabase-v3.test.ts mirrors the v2 live suite over eql_v3 domains:
  round-trips (incl. a Timestamptz column proving Date reconstruction),
  bulk models, text_search equality (full-envelope operand), free-text
  like→cs (include_original: false — load-bearing, see the suite header),
  int4_ord equality + gte/lte range, timestamptz_ord range with Date
  values. Same env gating as the v2 suite; the eql_v3 Exposed-schemas
  dashboard step is documented as the manual prerequisite.
- supabase.test.ts gains the v2 encrypted-range test (gte/lte on an
  orderAndRange number column) — the 'range filtering works on Supabase'
  claim previously rested on a one-off live spike with no CI baseline.
- installEqlV3IfNeeded accepts { supabase: true }: opclass-stripped
  bundle + eql_v3 grants, matching the CLI's --eql-version 3 --supabase.

Part of CIP-3300.
- stash-supabase skill: new 'EQL v3 (native eql_v3.* domains)' section —
  setup, per-domain DDL, --eql-version 3 install, the Exposed-schemas
  silent-fallback warning, v3-specific behaviour (full-envelope operands,
  like→cs, include_original: false for substring match), shared caveats.
- Recreate docs/reference/supabase-sdk.md (deleted in def9f4b; AGENTS.md
  'Useful Links' had a dangling reference) covering both adapters, the
  install + Exposed-schemas story, and the v3 encoding details.
- Changeset: minor for @cipherstash/stack and stash.

Part of CIP-3300.
… status/upgrade, drift gate

Runtime (stack adapter):
- reject null filter operands with a pointer to .is(col, null) —
  encrypt(null) short-circuited to null and JSON.stringify sent the
  literal string "null" as the operand
- Date reconstruction now covers user-chosen PostgREST aliases:
  addJsonbCastsV3 returns the result-key→DB-column map the select
  actually produces and postprocessDecryptedRow consumes it (static
  property/DB names remain the fallback for no-select paths)
- single source for the encrypted like/ilike→cs remap
  (encryptedFilterOp), consumed by applyPatternFilter,
  notFilterOperator, and transformOrConditions

Type safety (behavior change):
- the v3 builder's default Row is exactly InferPlaintext<Table> — the
  previous `& Record<string, unknown>` widening collapsed
  V3FilterableKeys to string, silently disabling the storage-only
  filter guard. Passthrough columns now need an explicit Row.
- match() is FK-narrowed (Partial<Pick<T, FK>>) like every other
  filter method

CLI:
- db status reports v2 and v3 installs independently (a v3-only DB no
  longer reads "EQL is not installed")
- db upgrade accepts --eql-version, rejects v3+--latest, and points at
  the other generation when the requested one isn't installed
- v3 path routing extracted to pure routeInstallPathForEqlVersion
  (drizzle fallback + migration-mode skip) with unit tests
- dry-run output names the v3 bundle; CRLF-safe @file regex in the
  bundle derivation script
- gen:eql-v3-sql script + CI regenerate-and-diff gate so the vendored
  bundles cannot drift from the fixture silently
- test helper reads the CLI-vendored Supabase bundle instead of
  duplicating the strip logic (live suite now installs exactly what
  `stash db install --eql-version 3 --supabase` installs)

Tests: +6 builder unit tests (or() structured/string/verbatim, null
guard, is-null passthrough, aliased-date), +2 type tests (default-Row
narrowing, match() FK), +3 CLI routing tests. Docs + changeset updated
for the Row semantics; stale src/schema/v3 path comment fixed.
The mega-table seed INSERT failed CI with 'value for domain
eql_v3.text_ord_ore violates check constraint "text_ord_ore_check"':
row A seeds every text domain with TEXT_S[0] = '', and the text_ord /
text_ord_ore / text_search domain CHECKs demand a non-empty ore term
(jsonb_array_length(VALUE->'ob') > 0) — the ORE term of the empty
string has zero blocks, so the domain cast rejects the row before it
ever lands. (text_eq and text_match accept '' — hm/bf don't have the
non-empty requirement — which is why the insert died exactly at the
first ob-requiring text column, $25.)

Give the ob-carrying text domains their own TEXT_ORD_S sample set
(non-empty; ord-equality and match-proof row-targeting constraints
preserved), keeping the '' edge covered on text / text_eq / text_match
where it is actually storable.
…before Date comparison

Unblocked by the previous commit (the seed INSERT now succeeds, so the
storage-tier proofs run in CI for the first time): the date/timestamptz
cases compared client.decrypt() output against a Date, but lone-
ciphertext decrypt has no column identity — cast_as-driven Date
reconstruction is a decrypt-model feature — so the FFI returns the
serialized instant ('2026-07-01T00:00:00Z'). Parse before comparing.
main took #543 (db install/upgrade/status → the eql command group with
deprecated db aliases) under this branch; update the v3 docs, skill,
changeset, and test-helper comments to reference the new spellings
(stash eql install --eql-version 3 --supabase etc.).
…ha.2 release

Replace the stale pre-release snapshot with the cipherstash-encrypt.sql
artifact of the eql-3.0.0-alpha.2 release (cipherstash/encrypt-query-language),
byte-for-byte, and regenerate both CLI copies.

Bundle changes picked up: SQL-standard domain renames (eql_v3.integer et al,
eql_v3.int4 gone), the eql_v3_internal schema split (SEM index-term internals
moved out of eql_v3), envelope CHECKs pinned to v='3', new *_ord_ope domain
variants, and jsonb domains (json/jsonb_entry/jsonb_query).

build-eql-v3-sql.mjs: provenance header now records the release artifact as
the source (the temporary-vendoring framing is superseded; the Supabase
variant is still derived locally because upstream ships none). The strip
assertion stays pinned at 2 operator_class.sql chunks — verified by
inspection, those 2 chunks carry all 4 CREATE OPERATOR CLASS/FAMILY
statements — with a new source-side assertion pinning the 4-statement count.

eql-v3.ts helper: the install sentinel is now generation-aware
(eql_v3.text_search exists in both generations, so alone it would silently
reuse a stale install; require eql_v3.timestamp + eql_v3_internal too), a
stale install is replaced by re-running the bundle (its leading DROP SCHEMA
CASCADE statements make that safe), and the Supabase grants now mirror onto
eql_v3_internal — the eql_v3 operators call SECURITY INVOKER functions there,
so without grants anon/authenticated queries fail 42501.
Follow-ups from the #547 review (coderdan):

- Reframe the full-envelope filter-operand behaviour as an explicit INTERIM
  workaround (tracked as Linear CIP-3402) in the stash-supabase skill, the
  Supabase SDK reference, and the changeset — spelling out the security
  caveat (operands carry a real decryptable ciphertext plus all index terms;
  PostgREST filters travel in GET query strings, so envelopes can land in URL
  logs, proxies, and Supabase request logs) and the planned fix (an EQL-side
  term-only scalar query envelope, the scalar analog of eql_v3.jsonb_query).
  The include_original: false substring-search requirement is marked as a
  symptom of the same interim mechanism. The technical explanation of why
  full envelopes are required today is unchanged.
- Sweep the SQL-standard domain renames through code samples, prose, and DDL
  (types.Int4Ord → types.IntegerOrd, types.TimestamptzOrd → types.TimestampOrd,
  types.Bool → types.Boolean, eql_v3.int4_ord → eql_v3.integer_ord,
  eql_v3.timestamptz_ord → eql_v3.timestamp_ord, …).
- Document the new surface: Exposed schemas stays eql_v3 ONLY; SEM internals
  now live in eql_v3_internal (never exposed, but role-granted by the
  installer); envelopes are v:3; the vendored bundle is the eql-3.0.0-alpha.2
  release artifact.
- Rewrite .changeset/eql-v3-supabase.md as one coherent description of the
  re-baselined feature (bundle, protect-ffi 0.27 eqlVersion, v:3 envelopes,
  SQL-standard names, interim operand caveat).
- Prepend superseded banners to the pre-alpha.2 design records under
  docs/superpowers (old domain names, v:2 envelope pins, no eql_v3_internal).
- Extend the encryptCollectedTerms doc comment (comment only) to pin it as
  the single swap point for the CIP-3402 term-only envelope: the encryption
  call and its JSON.stringify encoding are the only things to change;
  consuming seams are encoding-agnostic.
…(EQL#344)

Follow upstream cipherstash/encrypt-query-language#344, which renamed
every eql_v3 scalar domain to its SQL-standard name:

- int4*        -> integer*    (types.Integer*, EncryptedInteger*Column, INTEGER*)
- int2*        -> smallint*   (types.Smallint*, EncryptedSmallint*Column, SMALLINT*)
- float4*      -> real*       (types.Real*, EncryptedReal*Column, REAL*)
- float8*      -> double*     (types.Double*, EncryptedDouble*Column, DOUBLE*)
- timestamptz* -> timestamp*  (types.Timestamp*, EncryptedTimestamp*Column, TIMESTAMP*)
- bool         -> boolean     (types.Boolean, EncryptedBooleanColumn, BOOLEAN)

date*, text*, and numeric* are unchanged. bigint* and *_ord_ope stay
unimplemented pending FFI capability (CIP-3403).

Also fix the timestamp domains' cast_as: they previously emitted
cast_as: 'date', which the native FFI truncates to the calendar date,
losing time-of-day on decrypt. protect-ffi's CastAs has a distinct
'timestamp' (full date+time) variant, so:

- castAsEnum/eqlCastAsEnum/toEqlCastAs gain 'timestamp'
- the four TIMESTAMP* domains now emit cast_as: 'timestamp'
- decrypt-side Date reconstruction (typed client rowReconstructor and
  the Supabase v3 postprocessDecryptedRow) treats 'timestamp' like
  'date' (both decrypt to a JS Date)
- the previously-skipped live round-trip test for a timestamp column
  (schema-v3-client.test.ts) is re-enabled
…lient

Bump @cipherstash/protect-ffi 0.26.0 -> 0.27.0, which adds dual-format
EQL v2/v3 support via a newClient-level `eqlVersion` option
(cipherstash/protectjs-ffi#104).

- Add `eqlVersion?: 2 | 3` to ClientConfig and forward it to the FFI's
  newClient. `undefined` leaves the FFI's v2 default (and byte-identical
  v2 output) untouched.
- `Encryption()` auto-detects the wire version from the schema set using
  the v3 `buildColumnKeyMap()` marker: all-v3 -> 3, all-v2 -> FFI
  default. A mixed v2 + v3 schema set throws — one FFI client emits
  exactly one wire format. An explicit `config.eqlVersion` bypasses
  detection (migration escape hatch) but mixed sets still throw.
- `EncryptionV3()` sets `eqlVersion: 3` explicitly (override honoured).
- Widen the SDK `Encrypted` type to the FFI's `EncryptedPayload`
  (v2.3 | v3 storage payloads) and `EncryptedQuery` to include the v3
  `eql_v3.jsonb_query` needle, following the FFI 0.27 breaking type
  change; fix the operation-layer fallout.
- Split the lock-context guard suite into per-wire-format clients and
  cover the eqlVersion detection matrix in init-strategy tests.
…andshake guard, internal-schema grants

Integration pass over the four re-baseline slices (bundle re-vendor,
protect-ffi 0.27 + eqlVersion, domain renames, docs):

- live SQL suites call the SEM extractors at their new home
  (eql_v3_internal.hmac_256/bloom_filter/ore_block_256; the per-domain
  eql_v3.eq_term/ord_term/match_term wrappers are unchanged)
- INTERIM (CIP-3402): protect-ffi 0.27 has no v3 scalar query wire
  shape — scalar encryptQuery on a v3-wire client throws
  EQL_V3_QUERY_UNSUPPORTED — so the live suites build query terms with
  a second, explicitly v2-wire client (the extractors read only the
  term keys from jsonb; index terms are identical across wire formats).
  A new test pins the rejection behaviour the Supabase adapter's
  full-envelope operand path depends on.
- version-handshake guard: assertV3WireEnvelope fails loudly with a
  named generation-skew message (protect-ffi version / eqlVersion
  wiring / bundle generation) instead of an opaque 23514 — wired into
  the schema-v3-pg and matrix-live-pg seed paths. CLI side:
  isInstalled({eqlVersion: 3}) now requires BOTH eql_v3 and
  eql_v3_internal, so a stale pre-alpha.2 install is replaced instead
  of silently accepting wrong-generation wire data.
- SUPABASE_PERMISSIONS_SQL_V3 covers eql_v3 AND eql_v3_internal (the
  bundle is SECURITY INVOKER throughout, so calling roles need grants
  on the internal schema; Exposed schemas remains eql_v3 only)
- storage-envelope assertions updated to v:3; mock envelopes use the
  flat k-less v3 shape; the v3 builder's class doc now carries the
  interim framing of the full-envelope operand path
…der protect-ffi 0.27

protect-ffi < 0.27 threw a top-level pre-FFI parsing error for malformed
bulk ciphertexts; 0.27 moved ciphertext parsing into the fallible API
(structured as INVALID_CIPHERTEXT), so the operation succeeds and each
bad item carries its own { error } result. Update the expectation to
the new shape (surfaced by the first live CI run on the re-baseline).
--migrations-dir only feeds the Supabase v2 migration-file path; the v3
direct install proceeded without consuming it, silently ignoring the
flag. Reject it in validateInstallFlags alongside --drizzle/--migration/
--latest, with a validator test. (tobyhede review on #547, verified
CodeRabbit finding.)
…plaintext)

Add BIGINT/BIGINT_EQ/BIGINT_ORD_ORE/BIGINT_ORD domain definitions, 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).

Index emission follows the non-text numeric rule automatically:
bigint_eq -> unique (hm); bigint_ord/bigint_ord_ore -> ore only (equality
answered via ob).

Bounds are the full i64 range, enforced at the protect-ffi boundary.
GATED: live encrypt/decrypt requires the next protect-ffi release —
the pinned 0.27.0 JsPlaintext cannot marshal a JS bigint across the Neon
boundary (throws at runtime). No reconstruction is needed on the decrypt
paths: the model decrypt helpers pass FFI-returned values through with no
JSON round-trip.

Part of CIP-3291 (bigint work stream); *_ord_ope is out of scope (CIP-3403).
- catalog: add the four bigint domain rows with BIGINT_S samples covering
  i64::MAX (9223372036854775807n), i64::MIN (-9223372036854775808n), 0n,
  and a mid value beyond Number.MAX_SAFE_INTEGER; widen DomainSpec.samples
  to include bigint; add an optional liveGate field naming the skip reason.
- matrix-live / matrix-live-pg: exclude liveGate'd domains from the mega
  tables, seed rows, and term encryption (a bigint through protect-ffi
  0.27.0 throws at RUNTIME, which would fail the shared beforeAll and take
  every other domain's proof down); emit explicit reason-bearing skips
  instead. Gating lifts by removing liveGate when the protect-ffi pin bumps.
- matrix.test-d: bigint_ord column infers plaintext bigint and the
  equality|orderAndRange queryType union.
- schema-v3: bigint_ord/bigint_ord_ore join the equality-via-ORE and
  ore-only (no unique) regression lists.
- supabase-v3-builder (mock-based): bigint insert reaches encrypt as a real
  bigint, eq/gte filter operands JSON-encode safely (envelopes never carry
  raw bigint), and decrypted rows return a JS bigint through decryptResults.
  The mock envelope encodes bigint as a tagged string to preserve the
  real-world invariant that post-encryption envelopes are JSON-safe.
- supabase-v3.test-d: bigint column plaintext is bigint; filter and insert
  value types pin to bigint (number/string rejected).

Part of CIP-3291.
… gate

Add the bigint domain family to the Supabase SDK reference and the
stash-supabase skill (types table, DDL example, JS bigint in/out
semantics, i64 boundary-error behaviour), plus a changeset (minor) noting
the FFI-release gate and that bigint columns always decrypt to JS bigint.

Part of CIP-3291.
@changeset-bot

changeset-bot Bot commented Jul 5, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 9dd1a06

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

This PR includes changesets to release 6 packages
Name Type
@cipherstash/stack 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 5, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

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: 2cc3a17d-6a61-41ea-9b51-b92d41237821

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 james/cip-3291-bigint-stack

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.

@tobyhede tobyhede changed the base branch from main to feat/eql-v3-text-search-schema July 7, 2026 03:58
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.

3 participants