feat(stack,cli): EQL v3 Supabase adapter (encryptedSupabaseV3) + v3 install path#547
feat(stack,cli): EQL v3 Supabase adapter (encryptedSupabaseV3) + v3 install path#547freshtonic wants to merge 67 commits into
Conversation
🦋 Changeset detectedLatest commit: f3e4c3e The changes in this PR will be included in the next version bump. This PR includes changesets to release 7 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 |
📝 WalkthroughWalkthroughThis PR adds EQL v3 support across the stack: new v3 schema/table DSLs and public types, a typed v3 encryption client, Supabase v3 query handling, CLI ChangesEQL v3 Core and Client
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related issues
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 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 |
Consolidated & verified reviews from multiple sources
Every item below was re-verified against the branch by a dedicated agent. Verdicts: ✅ confirmed · 🔴 Runtime bugs (fix before merge)
🟠 Type-safety — the advertised v3 filter-key guard is currently a no-op
🟡 Test coverage
🟢 CI / release process
⚪ Maintainability / perf (minor)
Nits
❌ Not actionable
🤖 Generated with Claude Code |
… 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.
|
Thanks — addressed in 53d8421. Point-by-point below. Items whose flagged code predates this branch (main / the stacked base PRs) are dispositioned at the end rather than folded into this diff. 🔴 Runtime bugs
🟠 Type safety
🟡 Test coverage
🟢 CI / release
⚪ Maintainability
Nits
Verification: stack — 24 builder unit tests, 8 type tests, 0 |
|
Follow-up: the three deferred groups from my reply above now have their fixes up —
|
chore(stack): EQL v3 maintainability follow-ups from the #547 review
53d8421 to
51c25dc
Compare
- 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.
… 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.
|
Rebased this branch onto Rebase notes:
|
There was a problem hiding this comment.
Actionable comments posted: 6
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
packages/stack/src/supabase/query-builder.ts (2)
608-615: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winRoute raw encrypted filters through query-type and operator seams.
For v3,
.filter('email', 'like', value)is collected as an equality term and later sent aslike, bypassing the v3like/ilike→csremap. Add seams for raw-filter query type/operator so encrypted raw pattern filters usefreeTextSearchandcs.Also applies to: 918-922
🤖 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/supabase/query-builder.ts` around lines 608 - 615, Raw encrypted filters are being recorded as equality terms in the query builder, which bypasses the v3 query-type/operator remap for pattern matching. Update the raw-filter handling in QueryBuilder so the path that pushes terms and the corresponding raw term mapping carry the actual query type and operator seams instead of hardcoding equality/composite-literal. Use the existing QueryBuilder/raw-filter symbols and ensure `.filter(..., 'like'/'ilike', ...)` routes through freeTextSearch and remaps to cs for encrypted raw filters.
704-706: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winMap ordered columns through the dialect seam.
Filters/selects/mutations resolve v3 property names to DB names, but
order('createdAt')still reaches PostgREST ascreatedAtinstead ofcreated_at.Proposed fix
case 'order': - query = query.order(t.column, t.options) + query = query.order(this.filterColumnName(t.column), t.options) break🤖 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/supabase/query-builder.ts` around lines 704 - 706, The order clause in the query builder is bypassing the dialect seam, so v3 property names are not being mapped to database column names before reaching PostgREST. Update the `query-builder.ts` handling for the `order` case in the query-building flow to resolve `t.column` through the same dialect mapping used by filters/selects/mutations, then pass the translated DB column into `query.order(...)`. Make the change in the `order` branch of the query builder logic so `order('createdAt')` is sent as the DB name rather than the property name.
🧹 Nitpick comments (10)
packages/stack/src/types.ts (1)
253-255: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winUpdate the public
EncryptOptions.columndoc to match the widened contract.Line 253 still documents only
EncryptedColumn/EncryptedField, but Line 254 now accepts anyBuildableColumn, including v3 builders.Proposed doc update
export type EncryptOptions = { - /** The column or nested field to encrypt into. From {`@link` EncryptedColumn} or {`@link` EncryptedField}. */ + /** The column or nested field to encrypt into. Accepts v2/v3 buildable storage columns. */ column: BuildableColumn // storage: fields are encryptable, so stays wide table: BuildableTable }🤖 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 253 - 255, The public EncryptOptions.column documentation still implies only EncryptedColumn/EncryptedField, but the type now accepts any BuildableColumn. Update the doc comment on EncryptOptions.column in types.ts to describe the widened contract and mention that v3 builders are supported, keeping the wording aligned with the actual BuildableColumn type and related EncryptOptions symbols.packages/stack/__tests__/encrypt-lock-context-guards.test.ts (1)
51-55: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winEnv var set without cleanup across test runs.
process.env.CS_WORKSPACE_CRNis set inbeforeEachbut never restored/deleted in anafterEach. If other test files run in the same worker process, this can leak into unrelated suites and cause flaky/order-dependent behavior.🧹 Suggested cleanup
+afterEach(() => { + delete process.env.CS_WORKSPACE_CRN +}) + beforeEach(async () => { vi.clearAllMocks() process.env.CS_WORKSPACE_CRN = 'crn:ap-southeast-2.aws:test-workspace' client = await Encryption({ schemas: [users, usersV3] }) })🤖 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__/encrypt-lock-context-guards.test.ts` around lines 51 - 55, The test setup in beforeEach is mutating process.env.CS_WORKSPACE_CRN without cleaning it up, which can leak state into other suites. Update the encrypt-lock-context-guards.test.ts setup around beforeEach/afterEach to save the previous value, restore or delete CS_WORKSPACE_CRN after each test, and keep the environment isolated for the Encryption test cases. Use the existing vi.clearAllMocks and client setup as the anchor points when adding the cleanup.packages/stack/src/schema/index.ts (1)
354-366: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winUser-supplied match opts aliased by reference, unlike the v3 counterpart.
defaultsare fresh objects, but when a caller passesopts.tokenizer/opts.token_filters, they're stored by reference (not cloned). The v3EncryptedTextSearchColumn.freeTextSearch(which sharesdefaultMatchOpts) explicitly wraps its merged result incloneMatchOptsfor exactly this reason — a caller mutating their own opts object after calling.freeTextSearch(opts)can corrupt the column's stored index config. Worth aligning v2 with the same defensive copy for consistency and correctness.🔧 Suggested fix
freeTextSearch(opts?: MatchIndexOpts) { // Shared defaults (schema/match-defaults) — one source of truth with the // EQL v3 domain builders. The factory returns fresh nested objects. const defaults = defaultMatchOpts() - this.indexesValue.match = { - tokenizer: opts?.tokenizer ?? defaults.tokenizer, - token_filters: opts?.token_filters ?? defaults.token_filters, - k: opts?.k ?? defaults.k, - m: opts?.m ?? defaults.m, - include_original: opts?.include_original ?? defaults.include_original, - } + this.indexesValue.match = cloneMatchOpts({ + tokenizer: opts?.tokenizer ?? defaults.tokenizer, + token_filters: opts?.token_filters ?? defaults.token_filters, + k: opts?.k ?? defaults.k, + m: opts?.m ?? defaults.m, + include_original: opts?.include_original ?? defaults.include_original, + }) return this }(requires importing
cloneMatchOptsalongsidedefaultMatchOptsfrom./match-defaults)🤖 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/index.ts` around lines 354 - 366, The freeTextSearch method in Schema is storing caller-provided match options by reference, so later mutations to opts.tokenizer or opts.token_filters can leak into this.indexesValue.match. Update freeTextSearch to defensively clone the merged match options, matching the EncryptedTextSearchColumn.freeTextSearch behavior and using cloneMatchOpts with defaultMatchOpts. Also import cloneMatchOpts alongside defaultMatchOpts so the stored index config is isolated from external object mutation.packages/stack/__tests__/model-column-mapping.test.ts (1)
1-39: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winReaching into private helper for tests.
This test imports
resolveEncryptColumnMapdirectly from@/encryption/helpers/model-helpers, which is not re-exported from the publiceql/v3barrel. As per path instructions forpackages/**/__tests__/**/*.test.{ts,tsx}: "Prefer testing via public API; avoid reaching into private internals in tests."🤖 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__/model-column-mapping.test.ts` around lines 1 - 39, The test is importing a private helper directly instead of exercising the public API, so update it to cover the same behavior through the exported surface from encryptedTable/encryptedColumn and the eql/v3 barrel. Keep the assertions about JS property matching and DB-name resolution, but move the setup to the public model path so the test validates resolveEncryptColumnMap indirectly without importing it from model-helpers.Source: Path instructions
packages/stack/src/eql/v3/columns.ts (1)
297-324: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
EncryptedTextSearchColumn.build()duplicatesindexesForCapabilitiesoutput instead of reusing it.The override hand-rolls
{ unique: { token_filters: [] }, ore: {} }, which is exactly whatindexesForCapabilities(this.getQueryCapabilities(), 'string')would already produce forTEXT_SEARCHcapabilities (equality+orderAndRange withcastAs: 'string'). If the shared unique/ore derivation rules ever change (e.g. a new castAs branch), this override won't pick up the change and could silently diverge from the rest of the domain matrix.♻️ Proposed refactor to delegate to the shared helper
override build(): ColumnSchema { - // Deep-clone the match block so the returned config NEVER aliases this - // builder's internal `matchOpts` (or any caller-supplied opts merged into - // it). A caller mutating the returned object cannot corrupt this builder's - // state or another column's defaults. - return { - cast_as: 'string', - indexes: { - unique: { token_filters: [] }, - ore: {}, - match: cloneMatchOpts(this.matchOpts), - }, - } + // Deep-clone the match block so the returned config NEVER aliases this + // builder's internal `matchOpts` (or any caller-supplied opts merged into + // it). A caller mutating the returned object cannot corrupt this builder's + // state or another column's defaults. + return { + cast_as: 'string', + indexes: { + ...indexesForCapabilities(TEXT_SEARCH, 'string'), + match: cloneMatchOpts(this.matchOpts), + }, + } }Also applies to: 441-456
🤖 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/eql/v3/columns.ts` around lines 297 - 324, `EncryptedTextSearchColumn.build()` is duplicating the shared index-derivation logic instead of delegating to `indexesForCapabilities()`. Replace the hand-built indexes object in `EncryptedTextSearchColumn.build()` with a call to `indexesForCapabilities(this.getQueryCapabilities(), 'string')` so `TEXT_SEARCH` keeps matching the centralized capability rules, and ensure any other build paths that construct the same `{ unique, ore }` shape are updated to use the helper as well.packages/stack/__tests__/helpers/live-gate.ts (1)
14-29: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueModule-load-time env gates are import-order fragile.
LIVE_CIPHERSTASH_ENABLED/LIVE_EQL_V3_PG_ENABLEDare computed once at import time. The comment correctly documents that callers mustimport 'dotenv/config'first, but nothing enforces that ordering — a future test file that imports this helper beforedotenv/configwould silently getfalsegates instead of an error. Not urgent given the documented convention is followed everywhere today.🤖 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__/helpers/live-gate.ts` around lines 14 - 29, The live test gates in live-gate.ts are computed at module load time, so importing this helper before dotenv/config can silently freeze them to false. Update LIVE_CIPHERSTASH_ENABLED and LIVE_EQL_V3_PG_ENABLED to be evaluated after env loading, or add an explicit runtime check/error in the live gating helpers (describeLive, describeLivePg) so import order is no longer fragile.packages/stack/__tests__/v3-matrix/matrix-live-pg.test.ts (1)
175-213: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winParallelize per-domain query-term encryption in
beforeAll.The
eqDomains/ordDomains/matchDomainsloopsawait client.encryptQuery(...)sequentially, one network round-trip per domain (up to ~35). Batching withPromise.allwould cut setup latency significantly without changing behavior.⚡ Proposed fix
- for (const [t, spec] of eqDomains) { - eqTerms[slug(t)] = unwrapResult( - await client.encryptQuery( - spec.samples[0] as never, - { - table, - column: columnRef(t), - queryType: 'equality', - } as never, - ), - ) - } + await Promise.all( + eqDomains.map(async ([t, spec]) => { + eqTerms[slug(t)] = unwrapResult( + await client.encryptQuery( + spec.samples[0] as never, + { table, column: columnRef(t), queryType: 'equality' } as never, + ), + ) + }), + )Apply the same pattern to the
ordDomainsandmatchDomainsloops.🤖 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__/v3-matrix/matrix-live-pg.test.ts` around lines 175 - 213, The per-domain setup in `beforeAll` is doing `await client.encryptQuery(...)` sequentially in the `eqDomains`, `ordDomains`, and `matchDomains` loops, which adds unnecessary latency. Refactor the `eqTerms`, `ordTerms`, and `matchTerms` population in `matrix-live-pg.test.ts` to build promises per domain and resolve them with `Promise.all`, preserving the same `queryType` and `columnRef(t)` behavior while parallelizing the network round-trips.packages/stack/scripts/install-eql-v3.ts (1)
14-14: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winMove the shared installer helper out of
__tests__.This script performs live installation work but imports from the test tree. If tests are excluded from a packaged or script-only context,
install-eql-v3.tsstops resolving. Prefer a shared non-test helper and import it from both tests and this script.🤖 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 14, The live installer script currently depends on a helper inside the test tree, which can break when tests are not packaged or available. Move installEqlV3IfNeeded out of the __tests__ helpers into a shared non-test module, then update install-eql-v3.ts and any test callers to import that shared helper instead so the script resolves in script-only builds..github/workflows/fta-v3.yml (1)
34-35: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick winAdd
persist-credentials: falseto the read-only checkout.This job only reads source for static analysis (
permissions: contents: read, no push). The repo already appliespersist-credentials: falseon comparable read-only jobs (e.g.wasm-e2e-testsintests.yml); this new workflow should follow the same pattern to avoid leaving a usable token in the workspace.🔒 Proposed fix
- name: Checkout Repo uses: actions/checkout@v6 + with: + persist-credentials: false🤖 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 @.github/workflows/fta-v3.yml around lines 34 - 35, The Checkout Repo step in this read-only workflow should not leave credentials in the workspace, so update the actions/checkout usage to disable persisted auth. Add persist-credentials set to false on the checkout step in this workflow, matching the read-only pattern already used elsewhere, and keep the change scoped to the checkout action configuration.Source: Linters/SAST tools
packages/cli/src/commands/db/upgrade.ts (1)
18-37: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winExtract shared EQL version validation
upgrade.tsduplicates the same unknown--eql-versioncheck and the v3/--latestincompatibility already centralized ininstall.ts. Pulling this into a shared helper would keep the two commands aligned and avoid drift.🤖 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/src/commands/db/upgrade.ts` around lines 18 - 37, The EQL version checks in upgrade flow are duplicated and should be centralized to match the existing install command logic. Extract the shared validation from upgrade.ts into the same helper used by install.ts, then have the upgrade command call that helper instead of repeating the unknown `--eql-version` and `--eql-version 3` with `--latest` checks. Keep the behavior and error messaging consistent by reusing the same validation entry point referenced by the upgrade command’s version parsing and the install command’s option validation.
🤖 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 @.changeset/eql-v3-typed-client.md:
- Around line 20-29: Update the v3 release note in the changeset to remove the
bigint/int8 claim and keep it limited to Date reconstruction only. Adjust the
wording around the typed decrypt paths so it matches the current behavior in v3
and the deferred bigint handling in the v3 encryption code, without mentioning
bigint support in the release note.
In @.changeset/eql-v3-typed-schema.md:
- Around line 5-7: Update the changeset text in the EQL v3 schema builders note
to remove any mention of bigint support and keep the release note focused on
Date-only plaintext support; the v3 int8/bigint domain should not be advertised
yet. Keep the rest of the summary about `@cipherstash/stack/eql/v3`, `types`,
`getQueryCapabilities()` / `isQueryable()`, and the model encryption helpers
unchanged.
In `@packages/cli/src/commands/db/install.ts`:
- Around line 119-124: The permission check in EQL install is still using
v2-only logic, which can wrongly require CREATE on public for v3 installs.
Thread eqlVersion through EQLInstaller.checkPermissions and the call site in
db/install.ts, and update checkPermissions so it skips the
public.eql_v2_encrypted validation when eqlVersion is 3 while preserving the
existing v2 behavior.
- Around line 555-566: The EQL v3 validation in the install command currently
rejects --drizzle, --migration, and --latest, but still allows --migrations-dir
to slip through because v3 uses the direct install path. Update the
compatibility check in the install flow (the options.eqlVersion === '3' block in
db/install.ts) to also treat --migrations-dir as incompatible, using the same
return-message pattern as the existing flags so it is rejected before any direct
DB modification occurs.
In `@packages/stack/src/identity/index.ts`:
- Around line 41-47: The structural check in resolveLockContext currently uses
the in operator on input before নিশ্চিতing it is an object, which can throw for
null, undefined, or primitives. Update resolveLockContext in the identity module
to add an object/type guard before checking 'identityContext' in input, while
keeping the existing instanceof LockContext path so valid LockContext instances
still resolve correctly and malformed inputs fall through consistently.
In `@packages/stack/src/supabase/types.ts`:
- Around line 360-364: The structured .or() overload on EncryptedQueryBuilder
still accepts PendingOrCondition[] with untyped string columns, so callers can
target non-FK encrypted fields. Make PendingOrCondition generic over the allowed
column keys and update the EncryptedQueryBuilder.or() signature to use the
FK-bound version, keeping the same options shape while ensuring structured
filters are restricted to FK columns only.
---
Outside diff comments:
In `@packages/stack/src/supabase/query-builder.ts`:
- Around line 608-615: Raw encrypted filters are being recorded as equality
terms in the query builder, which bypasses the v3 query-type/operator remap for
pattern matching. Update the raw-filter handling in QueryBuilder so the path
that pushes terms and the corresponding raw term mapping carry the actual query
type and operator seams instead of hardcoding equality/composite-literal. Use
the existing QueryBuilder/raw-filter symbols and ensure `.filter(...,
'like'/'ilike', ...)` routes through freeTextSearch and remaps to cs for
encrypted raw filters.
- Around line 704-706: The order clause in the query builder is bypassing the
dialect seam, so v3 property names are not being mapped to database column names
before reaching PostgREST. Update the `query-builder.ts` handling for the
`order` case in the query-building flow to resolve `t.column` through the same
dialect mapping used by filters/selects/mutations, then pass the translated DB
column into `query.order(...)`. Make the change in the `order` branch of the
query builder logic so `order('createdAt')` is sent as the DB name rather than
the property name.
---
Nitpick comments:
In @.github/workflows/fta-v3.yml:
- Around line 34-35: The Checkout Repo step in this read-only workflow should
not leave credentials in the workspace, so update the actions/checkout usage to
disable persisted auth. Add persist-credentials set to false on the checkout
step in this workflow, matching the read-only pattern already used elsewhere,
and keep the change scoped to the checkout action configuration.
In `@packages/cli/src/commands/db/upgrade.ts`:
- Around line 18-37: The EQL version checks in upgrade flow are duplicated and
should be centralized to match the existing install command logic. Extract the
shared validation from upgrade.ts into the same helper used by install.ts, then
have the upgrade command call that helper instead of repeating the unknown
`--eql-version` and `--eql-version 3` with `--latest` checks. Keep the behavior
and error messaging consistent by reusing the same validation entry point
referenced by the upgrade command’s version parsing and the install command’s
option validation.
In `@packages/stack/__tests__/encrypt-lock-context-guards.test.ts`:
- Around line 51-55: The test setup in beforeEach is mutating
process.env.CS_WORKSPACE_CRN without cleaning it up, which can leak state into
other suites. Update the encrypt-lock-context-guards.test.ts setup around
beforeEach/afterEach to save the previous value, restore or delete
CS_WORKSPACE_CRN after each test, and keep the environment isolated for the
Encryption test cases. Use the existing vi.clearAllMocks and client setup as the
anchor points when adding the cleanup.
In `@packages/stack/__tests__/helpers/live-gate.ts`:
- Around line 14-29: The live test gates in live-gate.ts are computed at module
load time, so importing this helper before dotenv/config can silently freeze
them to false. Update LIVE_CIPHERSTASH_ENABLED and LIVE_EQL_V3_PG_ENABLED to be
evaluated after env loading, or add an explicit runtime check/error in the live
gating helpers (describeLive, describeLivePg) so import order is no longer
fragile.
In `@packages/stack/__tests__/model-column-mapping.test.ts`:
- Around line 1-39: The test is importing a private helper directly instead of
exercising the public API, so update it to cover the same behavior through the
exported surface from encryptedTable/encryptedColumn and the eql/v3 barrel. Keep
the assertions about JS property matching and DB-name resolution, but move the
setup to the public model path so the test validates resolveEncryptColumnMap
indirectly without importing it from model-helpers.
In `@packages/stack/__tests__/v3-matrix/matrix-live-pg.test.ts`:
- Around line 175-213: The per-domain setup in `beforeAll` is doing `await
client.encryptQuery(...)` sequentially in the `eqDomains`, `ordDomains`, and
`matchDomains` loops, which adds unnecessary latency. Refactor the `eqTerms`,
`ordTerms`, and `matchTerms` population in `matrix-live-pg.test.ts` to build
promises per domain and resolve them with `Promise.all`, preserving the same
`queryType` and `columnRef(t)` behavior while parallelizing the network
round-trips.
In `@packages/stack/scripts/install-eql-v3.ts`:
- Line 14: The live installer script currently depends on a helper inside the
test tree, which can break when tests are not packaged or available. Move
installEqlV3IfNeeded out of the __tests__ helpers into a shared non-test module,
then update install-eql-v3.ts and any test callers to import that shared helper
instead so the script resolves in script-only builds.
In `@packages/stack/src/eql/v3/columns.ts`:
- Around line 297-324: `EncryptedTextSearchColumn.build()` is duplicating the
shared index-derivation logic instead of delegating to
`indexesForCapabilities()`. Replace the hand-built indexes object in
`EncryptedTextSearchColumn.build()` with a call to
`indexesForCapabilities(this.getQueryCapabilities(), 'string')` so `TEXT_SEARCH`
keeps matching the centralized capability rules, and ensure any other build
paths that construct the same `{ unique, ore }` shape are updated to use the
helper as well.
In `@packages/stack/src/schema/index.ts`:
- Around line 354-366: The freeTextSearch method in Schema is storing
caller-provided match options by reference, so later mutations to opts.tokenizer
or opts.token_filters can leak into this.indexesValue.match. Update
freeTextSearch to defensively clone the merged match options, matching the
EncryptedTextSearchColumn.freeTextSearch behavior and using cloneMatchOpts with
defaultMatchOpts. Also import cloneMatchOpts alongside defaultMatchOpts so the
stored index config is isolated from external object mutation.
In `@packages/stack/src/types.ts`:
- Around line 253-255: The public EncryptOptions.column documentation still
implies only EncryptedColumn/EncryptedField, but the type now accepts any
BuildableColumn. Update the doc comment on EncryptOptions.column in types.ts to
describe the widened contract and mention that v3 builders are supported,
keeping the wording aligned with the actual BuildableColumn type and related
EncryptOptions symbols.
🪄 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: b63665e9-f34b-4934-afbe-ba963ecdddce
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (86)
.changeset/eql-v3-supabase.md.changeset/eql-v3-text-search.md.changeset/eql-v3-typed-client.md.changeset/eql-v3-typed-schema.md.github/workflows/fta-v3.yml.github/workflows/tests.ymldocs/query-api-walkthrough.mddocs/reference/supabase-sdk.mddocs/superpowers/plans/2026-06-30-eql-v3-text-search-schema-plan.mddocs/superpowers/plans/2026-07-01-eql-v3-typed-schema.mddocs/superpowers/specs/2026-06-30-eql-v3-text-search-schema-design.mddocs/superpowers/specs/2026-07-02-stryker-v3-ci-gate-design.mdpackages/cli/package.jsonpackages/cli/scripts/build-eql-v3-sql.mjspackages/cli/src/__tests__/installer.test.tspackages/cli/src/__tests__/supabase-migration.test.tspackages/cli/src/bin/main.tspackages/cli/src/commands/db/install.tspackages/cli/src/commands/db/status.tspackages/cli/src/commands/db/upgrade.tspackages/cli/src/installer/index.tspackages/cli/src/sql/cipherstash-encrypt-v3-supabase.sqlpackages/cli/src/sql/cipherstash-encrypt-v3.sqlpackages/cli/tests/helpers/run.tspackages/stack/__tests__/cjs-require.test.tspackages/stack/__tests__/encrypt-lock-context-guards.test.tspackages/stack/__tests__/fixtures/eql-v3/cipherstash-encrypt-v3.sqlpackages/stack/__tests__/helpers/eql-v3.tspackages/stack/__tests__/helpers/live-gate.tspackages/stack/__tests__/helpers/stub-auth-wasm-inline.tspackages/stack/__tests__/helpers/stub-protect-ffi-wasm-inline.tspackages/stack/__tests__/model-column-mapping.test.tspackages/stack/__tests__/schema-v3-client.test.tspackages/stack/__tests__/schema-v3-pg.test.tspackages/stack/__tests__/schema-v3.test-d.tspackages/stack/__tests__/schema-v3.test.tspackages/stack/__tests__/supabase-v3-builder.test.tspackages/stack/__tests__/supabase-v3.test-d.tspackages/stack/__tests__/supabase-v3.test.tspackages/stack/__tests__/supabase.test.tspackages/stack/__tests__/typed-client-v3.test-d.tspackages/stack/__tests__/typed-client-v3.test.tspackages/stack/__tests__/types-public-surface.test-d.tspackages/stack/__tests__/v3-matrix/catalog.tspackages/stack/__tests__/v3-matrix/matrix-audit.test-d.tspackages/stack/__tests__/v3-matrix/matrix-bulk.test.tspackages/stack/__tests__/v3-matrix/matrix-identity-live.test.tspackages/stack/__tests__/v3-matrix/matrix-keyset.test.tspackages/stack/__tests__/v3-matrix/matrix-live-pg.test.tspackages/stack/__tests__/v3-matrix/matrix-live.test.tspackages/stack/__tests__/v3-matrix/matrix-lock-context.test.tspackages/stack/__tests__/v3-matrix/matrix.test-d.tspackages/stack/__tests__/v3-matrix/matrix.test.tspackages/stack/__tests__/wasm-inline-column-name.test.tspackages/stack/__tests__/wasm-inline-new-client.test.tspackages/stack/__tests__/wasm-inline-strategy.test.tspackages/stack/package.jsonpackages/stack/scripts/install-eql-v3.tspackages/stack/src/encryption/helpers/infer-index-type.tspackages/stack/src/encryption/helpers/model-helpers.tspackages/stack/src/encryption/index.tspackages/stack/src/encryption/operations/bulk-encrypt-models.tspackages/stack/src/encryption/operations/bulk-encrypt.tspackages/stack/src/encryption/operations/encrypt-model.tspackages/stack/src/encryption/operations/encrypt-query.tspackages/stack/src/encryption/operations/encrypt.tspackages/stack/src/encryption/v3.tspackages/stack/src/eql/v3/columns.tspackages/stack/src/eql/v3/index.tspackages/stack/src/eql/v3/table.tspackages/stack/src/eql/v3/types.tspackages/stack/src/identity/index.tspackages/stack/src/schema/index.tspackages/stack/src/schema/match-defaults.tspackages/stack/src/supabase/helpers.tspackages/stack/src/supabase/index.tspackages/stack/src/supabase/query-builder-v3.tspackages/stack/src/supabase/query-builder.tspackages/stack/src/supabase/types.tspackages/stack/src/types-public.tspackages/stack/src/types.tspackages/stack/src/wasm-inline.tspackages/stack/tsconfig.typecheck.jsonpackages/stack/tsup.config.tspackages/stack/vitest.config.tsskills/stash-supabase/SKILL.md
| const eqlVersion: 2 | 3 = options.eqlVersion === '3' ? 3 : 2 | ||
|
|
||
| // 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. | ||
| const routing = routeInstallPathForEqlVersion(eqlVersion, resolved) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Thread eqlVersion into the permission check.
The v3 path still calls the v2-oriented checkPermissions(), which requires CREATE on public for public.eql_v2_encrypted. That can incorrectly block native eql_v3.* installs for roles that do not need public-schema type creation.
Suggested direction
- const permissions = await installer.checkPermissions()
+ const permissions = await installer.checkPermissions({ eqlVersion })Then make EQLInstaller.checkPermissions({ eqlVersion }) skip the public.eql_v2_encrypted check when eqlVersion === 3.
Also applies to: 189-190
🧰 Tools
🪛 ast-grep (0.44.0)
[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { execSync } from 'node:child_process'
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').
(detect-child-process-typescript)
🤖 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/src/commands/db/install.ts` around lines 119 - 124, The
permission check in EQL install is still using v2-only logic, which can wrongly
require CREATE on public for v3 installs. Thread eqlVersion through
EQLInstaller.checkPermissions and the call site in db/install.ts, and update
checkPermissions so it skips the public.eql_v2_encrypted validation when
eqlVersion is 3 while preserving the existing v2 behavior.
| if (options.eqlVersion === '3') { | ||
| const incompatible = options.drizzle | ||
| ? '--drizzle' | ||
| : options.migration | ||
| ? '--migration' | ||
| : options.latest | ||
| ? '--latest' | ||
| : null | ||
| if (incompatible) { | ||
| return `\`--eql-version 3\` does not support \`${incompatible}\` yet — v3 currently installs via the direct path only.` | ||
| } | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Reject --migrations-dir for EQL v3 too.
--migrations-dir is migration-path-only, but v3 routing skips migration selection and proceeds with direct install. With --supabase --eql-version 3 --migrations-dir ..., the flag is silently ignored and the DB is modified directly.
Proposed fix
const incompatible = options.drizzle
? '--drizzle'
: options.migration
? '--migration'
- : options.latest
- ? '--latest'
- : null
+ : options.migrationsDir !== undefined
+ ? '--migrations-dir'
+ : options.latest
+ ? '--latest'
+ : null📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (options.eqlVersion === '3') { | |
| const incompatible = options.drizzle | |
| ? '--drizzle' | |
| : options.migration | |
| ? '--migration' | |
| : options.latest | |
| ? '--latest' | |
| : null | |
| if (incompatible) { | |
| return `\`--eql-version 3\` does not support \`${incompatible}\` yet — v3 currently installs via the direct path only.` | |
| } | |
| } | |
| if (options.eqlVersion === '3') { | |
| const incompatible = options.drizzle | |
| ? '--drizzle' | |
| : options.migration | |
| ? '--migration' | |
| : options.migrationsDir !== undefined | |
| ? '--migrations-dir' | |
| : options.latest | |
| ? '--latest' | |
| : null | |
| if (incompatible) { | |
| return `\`--eql-version 3\` does not support \`${incompatible}\` yet — v3 currently installs via the direct path only.` | |
| } | |
| } |
🧰 Tools
🪛 ast-grep (0.44.0)
[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { execSync } from 'node:child_process'
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').
(detect-child-process-typescript)
🤖 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/src/commands/db/install.ts` around lines 555 - 566, The EQL v3
validation in the install command currently rejects --drizzle, --migration, and
--latest, but still allows --migrations-dir to slip through because v3 uses the
direct install path. Update the compatibility check in the install flow (the
options.eqlVersion === '3' block in db/install.ts) to also treat
--migrations-dir as incompatible, using the same return-message pattern as the
existing flags so it is rejected before any direct DB modification occurs.
| export function resolveLockContext(input: LockContextInput): Context { | ||
| return input instanceof LockContext ? input.identityContext : input | ||
| // Use a structural check as well as `instanceof` so a `LockContext` | ||
| // constructed in another realm (or from a duplicate module instance) is still | ||
| // resolved rather than slipping through as a raw `Context`. | ||
| return input instanceof LockContext || 'identityContext' in input | ||
| ? (input as LockContext).identityContext | ||
| : input |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
Guard the structural lock-context check before using in.
Line 45 can throw a TypeError for plain JS callers that pass null, undefined, or a primitive. Add an object guard so malformed inputs fail downstream consistently instead of crashing in the resolver.
Proposed guard
export function resolveLockContext(input: LockContextInput): Context {
// Use a structural check as well as `instanceof` so a `LockContext`
// constructed in another realm (or from a duplicate module instance) is still
// resolved rather than slipping through as a raw `Context`.
- return input instanceof LockContext || 'identityContext' in input
+ return (
+ input instanceof LockContext ||
+ (typeof input === 'object' && input !== null && 'identityContext' in input)
+ )
? (input as LockContext).identityContext
: input
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export function resolveLockContext(input: LockContextInput): Context { | |
| return input instanceof LockContext ? input.identityContext : input | |
| // Use a structural check as well as `instanceof` so a `LockContext` | |
| // constructed in another realm (or from a duplicate module instance) is still | |
| // resolved rather than slipping through as a raw `Context`. | |
| return input instanceof LockContext || 'identityContext' in input | |
| ? (input as LockContext).identityContext | |
| : input | |
| export function resolveLockContext(input: LockContextInput): Context { | |
| // Use a structural check as well as `instanceof` so a `LockContext` | |
| // constructed in another realm (or from a duplicate module instance) is still | |
| // resolved rather than slipping through as a raw `Context`. | |
| return ( | |
| input instanceof LockContext || | |
| (typeof input === 'object' && input !== null && 'identityContext' in input) | |
| ) | |
| ? (input as LockContext).identityContext | |
| : input | |
| } |
🤖 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/identity/index.ts` around lines 41 - 47, The structural
check in resolveLockContext currently uses the in operator on input before
নিশ্চিতing it is an object, which can throw for null, undefined, or primitives.
Update resolveLockContext in the identity module to add an object/type guard
before checking 'identityContext' in input, while keeping the existing
instanceof LockContext path so valid LockContext instances still resolve
correctly and malformed inputs fall through consistently.
| or( | ||
| conditions: PendingOrCondition[], | ||
| options?: { referencedTable?: string; foreignTable?: string }, | ||
| ): EncryptedQueryBuilder<T> | ||
| match(query: Partial<T>): EncryptedQueryBuilder<T> | ||
| ): EncryptedQueryBuilder<T, FK> | ||
| match(query: Partial<Pick<T, FK>>): EncryptedQueryBuilder<T, FK> |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Narrow structured .or() conditions to FK.
PendingOrCondition[] keeps column: string, so v3 callers can still compile filters against storage-only encrypted columns via structured .or(...). Make PendingOrCondition generic, then bind this overload to FK.
Proposed type direction
- or(
- conditions: PendingOrCondition[],
+ or<K extends FK>(
+ conditions: PendingOrCondition<K>[],
options?: { referencedTable?: string; foreignTable?: string },
): EncryptedQueryBuilder<T, FK>🤖 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/supabase/types.ts` around lines 360 - 364, The structured
.or() overload on EncryptedQueryBuilder still accepts PendingOrCondition[] with
untyped string columns, so callers can target non-FK encrypted fields. Make
PendingOrCondition generic over the allowed column keys and update the
EncryptedQueryBuilder.or() signature to use the FK-bound version, keeping the
same options shape while ensuring structured filters are restricted to FK
columns only.
|
CI is fully green ✅ — including the live Two fixes got it there, both real issues the previously-failing seed INSERT had masked:
Run: https://github.com/cipherstash/stack/actions/runs/28694787649 — Node 22/24, Bun, Deno WASM, E2E, CodeQL, OSV, FTA all pass. |
Two JS properties whose builders resolve to the same DB name (getName()) silently overwrote in the built config — the later column won and the first's config was dropped. Throw instead, matching the existing duplicate-tableName guard in buildEncryptConfig and the reserved-key guard in encryptedTable. Regression tests: `EncryptedTable.build()` and `buildEncryptConfig` both throw on a duplicate DB name (schema-v3.test.ts, eql_v3 encryptedTable block).
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.).
299b680 to
21f8f3e
Compare
|
Great work on the seam architecture — the pinned-default dialect seams with v2 regression tests and the deviations write-up made a big PR easy to review. Before this merges I want to line up three structural concerns, because they compound with work that's in flight upstream. 1. The vendored bundle is a stale snapshot of the v3 surfaceThe fixture (and both derived CLI copies) was generated before several changes on the
So a refresh is not a mechanical re-vendor: it renames every domain in 2. Everything this writes is v2-wire data — migration debt from the first insert
Related: the typed 3. Full-envelope filter operands — correctly diagnosed, but let's not harden it inDeviation 1's analysis is right (
The root fix is on the EQL side and it's mine to own: scalars need a query-shaped counterpart the way jsonb already has None of this blocks the adapter architecture or the tests — it's sequencing. My preference: re-baseline bundle + FFI first and land this against the current surface. If you'd rather merge now, let's capture 1–3 as tracked follow-ups and gate any release on the re-baseline. |
|
@coderdan thanks — agreed on all three, and converting this to draft until the re-baseline lands. Verified upstream state since your comment: protect-ffi v0.27.0 (with #104) and eql-3.0.0-alpha.2 were both released yesterday, so the prerequisites now exist. Plan, landing as new commits on this PR: Re-baseline (in progress now)
Your point 3: full-envelope operands get marked interim in the skill/reference/changeset, and Tracked follow-ups (Linear, under CIP-3300): the No release before the re-baseline; nothing merged in the interim writes v2-wire data anywhere. |
…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).
|
Re-baseline complete — CI fully green, including the live suites on real v:3 wire. Ready for re-review; marking this ready-for-review since the re-baseline-before-merge condition is now met. @coderdan point-by-point against your three concerns: 1. Stale bundle → re-vendored from the 2. v2-wire data → protect-ffi 0.27 + 3. Full-envelope operands → marked interim everywhere (skill, reference, changeset, and both code doc-sites), with the URL-log caveat spelled out, and Two 0.27 behaviour drifts surfaced by live CI and fixed: malformed bulk ciphertexts are now per-item Commits are ordered as a review story: re-vendor → docs → renames → FFI/eqlVersion → integration+guards → 0.27 drift fix. |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 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-06-30-eql-v3-text-search-schema-plan.md`:
- Around line 3-8: The new blockquote in the plan document has a blank line that
triggers MD028 and may split the quote into separate blocks. Update the quoted
section so the spacing inside the blockquote is consistent, using the blockquote
marker on every intended quoted line, including any blank separator if it should
remain part of the same quote. Locate the affected text in the superseded note
near the EQL v3 plan header and adjust only the Markdown formatting.
In `@docs/superpowers/plans/2026-07-01-eql-v3-typed-schema.md`:
- Around line 3-8: The top blockquote in this plan doc has a blank line that can
split it into separate quote blocks and trigger MD028. Update the quoted note
near the opening of the document so the blockquote remains continuous, using the
same quoted text and keeping it as a single paragraph; check the leading note
around the “Superseded (re-baseline, 2026-07-05)” text.
In `@docs/superpowers/specs/2026-06-30-eql-v3-text-search-schema-design.md`:
- Around line 3-8: The blockquote in the spec front matter has a blank line that
can split it into separate quote blocks and trigger MD028. Update the quoted
“Superseded…” note so the entire message renders as one continuous blockquote in
the docs file, keeping the same content but removing the blank-line separation
within or adjacent to the quote.
🪄 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: 2653329a-9b8e-4823-b5e5-a29790e5d04e
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (44)
.changeset/eql-v3-supabase.mddocs/reference/supabase-sdk.mddocs/superpowers/plans/2026-06-30-eql-v3-text-search-schema-plan.mddocs/superpowers/plans/2026-07-01-eql-v3-typed-schema.mddocs/superpowers/specs/2026-06-30-eql-v3-text-search-schema-design.mdpackages/cli/scripts/build-eql-v3-sql.mjspackages/cli/src/__tests__/installer.test.tspackages/cli/src/installer/index.tspackages/cli/src/sql/cipherstash-encrypt-v3-supabase.sqlpackages/cli/src/sql/cipherstash-encrypt-v3.sqlpackages/stack/__tests__/cjs-require.test.tspackages/stack/__tests__/encrypt-lock-context-guards.test.tspackages/stack/__tests__/error-codes.test.tspackages/stack/__tests__/fixtures/eql-v3/cipherstash-encrypt-v3.sqlpackages/stack/__tests__/helpers/eql-v3.tspackages/stack/__tests__/init-strategy.test.tspackages/stack/__tests__/schema-v3-client.test.tspackages/stack/__tests__/schema-v3-pg.test.tspackages/stack/__tests__/schema-v3.test-d.tspackages/stack/__tests__/schema-v3.test.tspackages/stack/__tests__/supabase-v3-builder.test.tspackages/stack/__tests__/supabase-v3.test-d.tspackages/stack/__tests__/supabase-v3.test.tspackages/stack/__tests__/typed-client-v3.test-d.tspackages/stack/__tests__/typed-client-v3.test.tspackages/stack/__tests__/v3-matrix/catalog.tspackages/stack/__tests__/v3-matrix/matrix-bulk.test.tspackages/stack/__tests__/v3-matrix/matrix-live-pg.test.tspackages/stack/__tests__/v3-matrix/matrix-live.test.tspackages/stack/__tests__/v3-matrix/matrix.test-d.tspackages/stack/__tests__/wasm-inline-normalize.test.tspackages/stack/package.jsonpackages/stack/src/encryption/helpers/index.tspackages/stack/src/encryption/index.tspackages/stack/src/encryption/operations/batch-encrypt-query.tspackages/stack/src/encryption/v3.tspackages/stack/src/eql/v3/columns.tspackages/stack/src/eql/v3/index.tspackages/stack/src/eql/v3/types.tspackages/stack/src/schema/index.tspackages/stack/src/supabase/index.tspackages/stack/src/supabase/query-builder-v3.tspackages/stack/src/types.tsskills/stash-supabase/SKILL.md
✅ Files skipped from review due to trivial changes (1)
- docs/reference/supabase-sdk.md
🚧 Files skipped from review as they are similar to previous changes (22)
- packages/stack/tests/v3-matrix/matrix.test-d.ts
- packages/stack/src/eql/v3/types.ts
- packages/stack/tests/typed-client-v3.test.ts
- packages/stack/tests/v3-matrix/matrix-live.test.ts
- packages/stack/tests/v3-matrix/matrix-bulk.test.ts
- packages/cli/scripts/build-eql-v3-sql.mjs
- packages/stack/tests/typed-client-v3.test-d.ts
- packages/stack/tests/schema-v3-client.test.ts
- packages/stack/src/supabase/index.ts
- packages/stack/tests/v3-matrix/catalog.ts
- packages/stack/src/supabase/query-builder-v3.ts
- packages/stack/tests/schema-v3.test-d.ts
- packages/stack/tests/supabase-v3.test.ts
- packages/stack/src/encryption/v3.ts
- packages/stack/src/types.ts
- packages/stack/tests/v3-matrix/matrix-live-pg.test.ts
- packages/stack/tests/schema-v3.test.ts
- packages/stack/tests/encrypt-lock-context-guards.test.ts
- packages/stack/tests/supabase-v3.test-d.ts
- skills/stash-supabase/SKILL.md
- packages/stack/tests/schema-v3-pg.test.ts
- packages/stack/tests/supabase-v3-builder.test.ts
| > **Superseded (re-baseline, 2026-07-05):** written against the | ||
| > pre-`eql-3.0.0-alpha.2` EQL v3 surface — old domain names (`int4`, | ||
| > `timestamptz`, …, since renamed to SQL-standard `integer`, `timestamp`, …), | ||
| > `v: 2` envelope pins, and no `eql_v3_internal` schema. Retained as a | ||
| > historical design record; the shipped surface is the re-baselined one. | ||
|
|
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Blank line inside blockquote (MD028).
Static analysis flags a blank line inside/adjacent to the new blockquote, which can cause inconsistent rendering (split into two quote blocks) in some Markdown renderers.
📝 Suggested fix
> **Superseded (re-baseline, 2026-07-05):** written against the
> pre-`eql-3.0.0-alpha.2` EQL v3 surface — old domain names (`int4`,
> `timestamptz`, …, since renamed to SQL-standard `integer`, `timestamp`, …),
> `v: 2` envelope pins, and no `eql_v3_internal` schema. Retained as a
> historical design record; the shipped surface is the re-baselined one.
->
+>Ensure the blank line is either prefixed with > (to keep it part of the same blockquote) or replaced with a real blank line plus separation so it isn't parsed as splitting two blockquotes.
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)
[warning] 8-8: Blank line inside blockquote
(MD028, no-blanks-blockquote)
🤖 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-06-30-eql-v3-text-search-schema-plan.md` around
lines 3 - 8, The new blockquote in the plan document has a blank line that
triggers MD028 and may split the quote into separate blocks. Update the quoted
section so the spacing inside the blockquote is consistent, using the blockquote
marker on every intended quoted line, including any blank separator if it should
remain part of the same quote. Locate the affected text in the superseded note
near the EQL v3 plan header and adjust only the Markdown formatting.
Source: Linters/SAST tools
| > **Superseded (re-baseline, 2026-07-05):** written against the | ||
| > pre-`eql-3.0.0-alpha.2` EQL v3 surface — old domain names (`int4`, | ||
| > `timestamptz`, …, since renamed to SQL-standard `integer`, `timestamp`, …), | ||
| > `v: 2` envelope pins, and no `eql_v3_internal` schema. Retained as a | ||
| > historical design record; the shipped surface is the re-baselined one. | ||
|
|
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Blank line inside blockquote (MD028).
Same issue as the sibling plan doc — the blank line inside/adjacent to the new blockquote can split rendering into two quote blocks.
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)
[warning] 8-8: Blank line inside blockquote
(MD028, no-blanks-blockquote)
🤖 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` around lines 3 - 8,
The top blockquote in this plan doc has a blank line that can split it into
separate quote blocks and trigger MD028. Update the quoted note near the opening
of the document so the blockquote remains continuous, using the same quoted text
and keeping it as a single paragraph; check the leading note around the
“Superseded (re-baseline, 2026-07-05)” text.
Source: Linters/SAST tools
| > **Superseded (re-baseline, 2026-07-05):** written against the | ||
| > pre-`eql-3.0.0-alpha.2` EQL v3 surface — old domain names (`int4`, | ||
| > `timestamptz`, …, since renamed to SQL-standard `integer`, `timestamp`, …), | ||
| > `v: 2` envelope pins, and no `eql_v3_internal` schema. Retained as a | ||
| > historical design record; the shipped surface is the re-baselined one. | ||
|
|
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Blank line inside blockquote (MD028).
Same issue as the sibling docs — the blank line inside/adjacent to the new blockquote can split rendering into two quote blocks.
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)
[warning] 8-8: Blank line inside blockquote
(MD028, no-blanks-blockquote)
🤖 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/specs/2026-06-30-eql-v3-text-search-schema-design.md` around
lines 3 - 8, The blockquote in the spec front matter has a blank line that can
split it into separate quote blocks and trigger MD028. Update the quoted
“Superseded…” note so the entire message renders as one continuous blockquote in
the docs file, keeping the same content but removing the blank-line separation
within or adjacent to the quote.
Source: Linters/SAST tools
Summary
Implements EQL v3 on Supabase per the design spec in #546 (CIP-3300):
encryptedSupabaseV3at parity with the v2encryptedSupabaseadapter, using nativeeql_v3.*domain columns. Stacked onfeat/eql-v3-types-module(#541), targeting that branch.Core principle preserved from the spec: v3 mirrors v2 exactly — same query mechanism (direct EQL operators over PostgREST), same operator-visibility caveat + Exposed-schemas requirement, same install/permissions story. Only the column types and the wire encoding differ.
What's here
SDK adapter (
@cipherstash/stack/supabase)encryptedSupabaseV3+EncryptedQueryBuilderV3Impl, a narrow subclass ofEncryptedQueryBuilderImpl. The base class gains protected dialect seams whose defaults preserve v2 byte-for-byte (pinned by v2 regression tests); the v3 subclass overrides: column recognition, property↔DB name resolution (buildColumnKeyMap, aliasedprop:db::jsonbselect casts), raw-jsonb mutation payloads (no composite wrap), full-envelope filter operands,like/ilike→ PostgRESTcs, andDatereconstruction fromcast_as.InferPlaintext<Table> & Record<string, unknown>; with an explicit row type, storage-only columns (e.g.types.Bool) are excluded from filter methods at the type level (runtime guard always applies). The sharedEncryptedQueryBuildergains an optional filterable-keys generic (defaulted — v2 signatures unchanged).DB bundle + install
packages/cli/src/sql/via a checked-in derivation script (fallback strategy from the spec; the Supabase variant strips the two opclass chunks at their--! @filemarkers, mirroring upstream's**/*operator_class.sqlexclusion glob; sync risk documented in the script).stash db install --eql-version 3 [--supabase]— direct path only for now (--drizzle/--migration/--latestrejected with clear errors). Grants generalized:supabasePermissionsSql(schemaName)shared by v2/v3 (SUPABASE_PERMISSIONS_SQLunchanged,SUPABASE_PERMISSIONS_SQL_V3added).installEqlV3IfNeeded(test helper) is Supabase-aware:{ supabase: true }= stripped bundle +eql_v3grants.Tests
supabase-v3.test-d.ts).supabase-v3.test.tsmirroring the v2 suite (same env gating): round-trips incl. aTimestamptzcolumn provingDatereconstruction, bulk models,text_searchequality, free-textlike→cs,int4_ordequality + range,timestamptz_ordrange withDatevalues.supabase.test.ts(gte/lteon anorderAndRangenumber column) — the spec flagged that encrypted range on Supabase had no CI-covered v2 baseline.Docs
stash-supabaseskill: new EQL v3 section (setup, DDL mapping, install, the Exposed-schemas silent-fallback warning, v3 behaviour, shared caveats).docs/reference/supabase-sdk.md(deleted indef9f4bd; fixes the dangling AGENTS.md link).@cipherstash/stack+stash.Deviations from the spec (found in the bundle source, not guessed)
text_searchequality. The spec assumed single-capability domains' terms satisfy their own CHECKs (e.g. "anint4_ordrange term carriesob, which is exactly whatint4_ordrequires"). They don't: everyeql_v3.*domain CHECK also requiresv,i, andc(see e.g. theint4_ord/text_eqCHECKs in the bundle), and protect-ffi'sEncryptedScalarQueryis typedc?: never— query terms can never pass any domain CHECK. The(domain, jsonb)operator functions also cast their operand into the domain (b::eql_v3.text_search), so there is no coercion-free path. The adapter therefore encrypts all filter operands with the storage path; the operators extract the term they need (eq_term/ord_term/match_term). This also resolves the spec's open question fix(ci): set up bun in github actions for release #2 — the full envelope isn't just the cleanest way, it's the only way.like/ilikecannot staylikeon the wire. The v3 domains define=,<>,<,<=,>,>=,@>,<@but no~~. Encrypted pattern filters are emitted as PostgRESTcs(@>onmatch_term). Match is tokenized + downcased solike/ilikeare equivalent;%wildcards should not be used.include_original: falseon the column's match index: with the defaulttrue, the full-envelope operand's bloom carries the whole pattern as an extra token, so substring patterns only match when equal to the stored value. Documented in the skill/reference; the live test's schema sets it explicitly with a load-bearing comment.Verification
packages/stack: 0src/type errors; 25 unit tests + 7 type tests pass; live suites parse and gate correctly (skipped without env). Pre-existing__tests__type errors andNot authenticatedlive-test failures on this branch are unchanged (no localCS_*/Supabase creds — same count at baseline).packages/cli: typecheck error count identical to baseline (all pre-existing, unbuilt workspace deps); 44 tests pass; both packagestsupbuild cleanly, v3 bundles ship indist/sql/.eql_v3in Exposed schemas +CS_*creds. The full-envelope equality behaviour matches the spec's live spike; the range/free-text assertions should be confirmed on the first live run.Out of scope (per spec)
Plaintext→encrypted migration lifecycle; encrypted
ORDER BY(OPE terms are the forward path); v3 in the Drizzle/Supabase-migration-file install paths; the spec's proposed install-time exposed-schema probe (documented as a firm follow-up — worth its own issue since it retroactively protects v2 too).Closes CIP-3300. Design: #546.
Summary by CodeRabbit
--eql-version 2|3, generation-aware install/upgrade/status, and SupabaseencryptedSupabaseV3./v3) plus a v3 authoring DSL (/eql/v3) with strengthened TypeScript inference andDateplaintext support.Datereconstruction.