feat(stack): Bigint domains + Supabase support (i64-bounded) — CIP-3291 [gated on protect-ffi release]#557
Conversation
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
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 detectedLatest commit: 9dd1a06 The changes in this PR will be included in the next version bump. This PR includes changesets to release 6 packages
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 |
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
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 tomain(merge commit93b29291is main's tip), so this PR targetsmaindirectly and its diff is exactly the bigint work.Recorded decisions (from the Linear ticket — not relitigated here)
-2^63 … 2^63 - 1). Enforcement lives at the protect-ffi boundary (a parallel PR adds JSBigIntto protect-ffi'sJsPlaintexton 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, always (never a precision-lossynumber).*_ord_ope(CIP-3403): this addsbigint,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-ffi0.27.0 does not accept/return JSbigint(JsPlaintextlacks it): encrypting abigintthrows at runtime. The pin is deliberately NOT bumped here. This PR un-drafts when ALL of:BigIntinJsPlaintextships (parallel FFI PR on the eql_v3 release line);liveGatefrom 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_ORDdomain consts (eql_v3.bigint*,castAs: 'bigint'),EncryptedBigint*Columnclasses,AnyEncryptedV3Columnmembership,PlaintextKind/PlaintextFromKindgain'bigint'→bigint.eql/v3/types.ts:types.Bigint/BigintEq/BigintOrdOre/BigintOrdfactories (naming convention preserved); barrel exports ineql/v3/index.ts.types.ts:Plaintext = JsPlaintext | Date | bigint, with the gate documented.bigint_eq→unique(hm);bigint_ord/bigint_ord_ore→oreonly (equality answered viaob, likeinteger_ord). Pinned by regression tests.schema/index.tsalready had'bigint'→'big_int'incastAsEnum/toEqlCastAs(and the wasmnormalizeCastAspath) — no change needed.Supabase adapter: no runtime code change needed —
query-builder-v3.tsis value-generic (filter operands go throughencrypt(); only post-encryption envelopes are JSON-encoded, and envelopes never contain a rawbigint). Coverage added instead (below).Decrypt-path bigint survival (verified): the model decrypt path (
encryption/helpers/model-helpers.ts→ FFIdecryptBulk) and the SupabasedecryptResults→decryptModel/bulkDecryptModels→postprocessDecryptedRowpath pass FFI-returned values through with no JSON round-trip — a JSbigintreturned through the Neon boundary reaches the caller intact.rowReconstructorneeds no bigint arm (comment updated). The onlyJSON.stringifyon the query path operates on post-encryption envelopes (safe).Tests:
v3-matrix/catalog.ts: 4 bigint rows (compile-time coverage gate satisfied);BIGINT_Ssamples include9223372036854775807n(i64::MAX),-9223372036854775808n(i64::MIN),0n, and4611686018427387904n(> 2^53, proves losslessness); newDomainSpec.liveGatefield 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 inbeforeAll, which would take every other domain's live proof down — and reported as explicit skips.matrix.test-d.ts:bigint_ordinfers plaintextbigint+'equality' | 'orderAndRange'.schema-v3.test.ts: bigint joins the equality-via-ORE and ore-only (nounique) regression lists.supabase-v3-builder.test.ts(mock, wire-encoding):types.BigintOrdcolumn — insert delivers the realbigintto encrypt;eq/gteoperands JSON-encode safely as full envelopes; decrypted rows return a JSbigintthroughdecryptResults.supabase-v3.test-d.ts: bigint column plaintext type isbigint; filter/insert value types pin tobigint(number/string rejected).Docs:
docs/reference/supabase-sdk.md+skills/stash-supabase/SKILL.mdgain 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).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 +
liveGateremoval + live verification un-drafts it.