Skip to content

feat!: remove EQL v2 repo-wide (umbrella, #707) - #772

Merged
coderdan merged 127 commits into
mainfrom
remove-v2
Jul 29, 2026
Merged

feat!: remove EQL v2 repo-wide (umbrella, #707)#772
coderdan merged 127 commits into
mainfrom
remove-v2

Conversation

@tobyhede

@tobyhede tobyhede commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Umbrella for #707: remove the EQL v2 authoring and emission surface repo-wide while preserving reads of existing v2 ciphertext.

Current state

The removal work is integrated on remove-v2. This status supersedes the earlier in-flight branch snapshot in this issue's edit history.

  • New schemas and adapter writes use EQL v3.
  • Existing EQL v2 payloads remain decryptable through @cipherstash/stack.
  • The canonical client is Encryption from @cipherstash/stack/v3.
  • Drizzle uses the @cipherstash/stack-drizzle package root with unsuffixed exports.
  • Supabase uses encryptedSupabase from @cipherstash/stack-supabase.
  • DynamoDB writes v3, reads existing v2 items through the native client, and typed decrypt operations retain .audit().

Final release gate

#816 owns the remaining adapter correctness and release-verification pass: fail-closed Supabase filters, Date reconstruction parity, examples/e2e/integration typechecks, final changeset and shipped-skill reconciliation, and CLI/wizard skill parity.

Already-consumed RC changesets remain historical records. Final pending changesets and migration guidance are reconciled to the canonical 1.0 surface rather than rewriting published RC history.

Closes #707 when the final release gate is merged.

@changeset-bot

changeset-bot Bot commented Jul 23, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: bf7be09

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

This PR includes changesets to release 13 packages
Name Type
@cipherstash/stack Major
@cipherstash/stack-supabase Major
stash Major
@cipherstash/wizard Major
@cipherstash/stack-drizzle Major
@cipherstash/prisma-next Major
@cipherstash/migrate Major
@cipherstash/nextjs Patch
@cipherstash/bench Patch
@cipherstash/test-kit Patch
@cipherstash/basic-example Patch
@cipherstash/prisma-next-example Patch
@cipherstash/e2e Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown

Important

Review skipped

Too many files!

This PR contains 365 files, which is 265 over the limit of 100.

To get a review, narrow the scope:
• coderabbit review --committed # exclude uncommitted changes
• coderabbit review --dir # limit to a subdirectory
• coderabbit review --base # compare against a closer base

Upgrade to a paid plan to raise the limit.

Usage-priced reviews support at most 300 files.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 37647c3c-c08e-4443-95f2-6ba9d3cc98e6

📥 Commits

Reviewing files that changed from the base of the PR and between 8d32ba6 and bf7be09.

⛔ Files ignored due to path filters (3)
  • packages/cli/__fixtures__/scaffold/drizzle.generated.ts is excluded by !**/*.generated.*
  • packages/cli/__fixtures__/scaffold/generic.generated.ts is excluded by !**/*.generated.*
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (470)
  • .changeset/adapter-release-readiness.md
  • .changeset/backfill-missing-column-message.md
  • .changeset/cli-near-miss-statement-trim.md
  • .changeset/cli-v2-cutover-prompt-correction.md
  • .changeset/decrypt-chaining-docs.md
  • .changeset/domain-carrier-not-public.md
  • .changeset/drizzle-encrypted-indexes.md
  • .changeset/dynamodb-eql-v3.md
  • .changeset/dynamodb-skill-wasm-caveat.md
  • .changeset/dynamodb-v2-read-table-forwarding.md
  • .changeset/dynamodb-wasm-v2-read.md
  • .changeset/encrypt-client-guard-parity.md
  • .changeset/encrypt-lifecycle-mixed-table.md
  • .changeset/encryption-schema-arrays.md
  • .changeset/eql-v3-cli-install.md
  • .changeset/eql-v3-sole-docs.md
  • .changeset/init-drizzle-eql-v3.md
  • .changeset/init-placeholder-eql-v3.md
  • .changeset/init-scaffold-compiles.md
  • .changeset/migrate-column-exists.md
  • .changeset/migrate-eql-v3.md
  • .changeset/nextjs-stack-metadata.md
  • .changeset/one-sided-eql-detection-docs.md
  • .changeset/pre.json
  • .changeset/prisma-next-v3-client-config.md
  • .changeset/reject-v2-wire-over-v3-schemas.md
  • .changeset/remove-cli-migrate-v2-leaf.md
  • .changeset/remove-eql-v2-drizzle-root.md
  • .changeset/remove-eql-v2-migrate-classifier.md
  • .changeset/remove-eql-v2-packages.md
  • .changeset/remove-eql-v2-scaffold-examples-meta.md
  • .changeset/remove-eql-v2-supabase-authoring.md
  • .changeset/remove-eql-v2-supabase-skill.md
  • .changeset/rewriter-never-drops-ciphertext.md
  • .changeset/schema-stevec-standard-pin.md
  • .changeset/skills-encryption-client-naming.md
  • .changeset/skills-ship-current-copy.md
  • .changeset/skills-v3-lifecycle-honesty.md
  • .changeset/stack-audit-on-decrypt.md
  • .changeset/stack-dynamodb-v2-write-removal.md
  • .changeset/stack-logger-edge-safe.md
  • .changeset/stack-skills-eql-v3-audit.md
  • .changeset/stash-cli-eql-v3-default.md
  • .changeset/stash-cli-skill-refresh.md
  • .changeset/stash-postgres-edge-skills.md
  • .changeset/supabase-interface-row-types.md
  • .changeset/supabase-single-row-typing.md
  • .changeset/supabase-structural-v3-columns.md
  • .changeset/supabase-v2-table-diagnosis.md
  • .changeset/typecheck-gates-scope-source.md
  • .changeset/typed-client-init-parity.md
  • .changeset/typed-encrypt-query-dist.md
  • .changeset/wizard-eql-v3-migration-rewrite.md
  • .github/dependabot.yml
  • .github/workflows/fta-v3.yml
  • .github/workflows/integration-drizzle.yml
  • .github/workflows/integration-prisma-next.yml
  • .github/workflows/integration-supabase.yml
  • .github/workflows/prisma-next-e2e.yml
  • .github/workflows/rebuild-docs.yml
  • .github/workflows/tests-bench.yml
  • .github/workflows/tests.yml
  • AGENTS.md
  • README.md
  • SECURITY.md
  • docs/plans/cli-help-and-manifest.md
  • docs/plans/encryption-migrations.md
  • docs/query-api-walkthrough.md
  • docs/reference/supabase-sdk.md
  • docs/superpowers/specs/2026-07-24-a2-fail-closed-rewrite-design.md
  • docs/superpowers/specs/2026-07-24-encryption-signature-and-typecheck-gates-design.md
  • e2e/README.md
  • e2e/package.json
  • e2e/tests/package-managers.e2e.test.ts
  • e2e/tests/skill-map-parity.e2e.test.ts
  • examples/basic/encrypt.ts
  • examples/basic/index.ts
  • examples/basic/package.json
  • examples/basic/src/encryption/index.ts
  • examples/basic/src/lib/supabase/encrypted.ts
  • examples/basic/src/lib/supabase/server.ts
  • examples/basic/src/queries/contacts.ts
  • examples/basic/tsconfig.json
  • package.json
  • packages/bench/README.md
  • packages/bench/__benches__/drizzle/operators.bench.ts
  • packages/bench/__tests__/drizzle/operators.explain.test.ts
  • packages/bench/__unit__/seed-keys.test.ts
  • packages/bench/package.json
  • packages/bench/sql/schema.sql
  • packages/bench/src/drizzle/setup.ts
  • packages/bench/src/harness/global-setup.ts
  • packages/bench/src/harness/seed.ts
  • packages/bench/vitest.config.ts
  • packages/bench/vitest.unit.config.ts
  • packages/cli/AGENTS.md
  • packages/cli/README.md
  • packages/cli/package.json
  • packages/cli/scripts/e2e-encrypt.sh
  • packages/cli/scripts/fixtures/seed-users.sql
  • packages/cli/src/__tests__/installer.test.ts
  • packages/cli/src/__tests__/placeholder-guard-parity.test.ts
  • packages/cli/src/__tests__/release-train.test.ts
  • packages/cli/src/__tests__/rewrite-migrations.test.ts
  • packages/cli/src/__tests__/supabase-migration.test.ts
  • packages/cli/src/__tests__/v2-retirement.test.ts
  • packages/cli/src/bin/main.ts
  • packages/cli/src/cli/__tests__/help.test.ts
  • packages/cli/src/cli/registry.ts
  • packages/cli/src/commands/db/__tests__/find-generated-migration.test.ts
  • packages/cli/src/commands/db/__tests__/generate-drizzle-migration.test.ts
  • packages/cli/src/commands/db/activate.ts
  • packages/cli/src/commands/db/config-scaffold.ts
  • packages/cli/src/commands/db/install.ts
  • packages/cli/src/commands/db/push.ts
  • packages/cli/src/commands/db/rewrite-migrations.ts
  • packages/cli/src/commands/db/status.ts
  • packages/cli/src/commands/db/supabase-migration.ts
  • packages/cli/src/commands/db/upgrade.ts
  • packages/cli/src/commands/encrypt/__tests__/backfill-target.test.ts
  • packages/cli/src/commands/encrypt/__tests__/context-placeholder.test.ts
  • packages/cli/src/commands/encrypt/__tests__/encrypt-v3.test.ts
  • packages/cli/src/commands/encrypt/backfill.ts
  • packages/cli/src/commands/encrypt/context.ts
  • packages/cli/src/commands/encrypt/cutover.ts
  • packages/cli/src/commands/encrypt/drizzle-helper.ts
  • packages/cli/src/commands/encrypt/drop.ts
  • packages/cli/src/commands/encrypt/lib/__tests__/resolve-eql.test.ts
  • packages/cli/src/commands/encrypt/lib/resolve-eql.ts
  • packages/cli/src/commands/eql/__tests__/migration.test.ts
  • packages/cli/src/commands/eql/migration.ts
  • packages/cli/src/commands/impl/index.ts
  • packages/cli/src/commands/init/__tests__/init-command.test.ts
  • packages/cli/src/commands/init/__tests__/placeholder-client-fixture.test.ts
  • packages/cli/src/commands/init/__tests__/utils-codegen-drizzle.test.ts
  • packages/cli/src/commands/init/__tests__/utils-codegen.test.ts
  • packages/cli/src/commands/init/doctrine/AGENTS-doctrine.md
  • packages/cli/src/commands/init/index.ts
  • packages/cli/src/commands/init/lib/__tests__/install-skills.test.ts
  • packages/cli/src/commands/init/lib/__tests__/introspect.test.ts
  • packages/cli/src/commands/init/lib/__tests__/setup-prompt.test.ts
  • packages/cli/src/commands/init/lib/install-skills.ts
  • packages/cli/src/commands/init/lib/introspect.ts
  • packages/cli/src/commands/init/lib/read-context.ts
  • packages/cli/src/commands/init/lib/setup-prompt.ts
  • packages/cli/src/commands/init/lib/write-context.ts
  • packages/cli/src/commands/init/providers/__tests__/drizzle.test.ts
  • packages/cli/src/commands/init/providers/drizzle.ts
  • packages/cli/src/commands/init/steps/__tests__/install-eql.test.ts
  • packages/cli/src/commands/init/steps/__tests__/resolve-proxy-choice.test.ts
  • packages/cli/src/commands/init/steps/install-eql.ts
  • packages/cli/src/commands/init/steps/resolve-proxy-choice.ts
  • packages/cli/src/commands/init/types.ts
  • packages/cli/src/commands/init/utils.ts
  • packages/cli/src/commands/plan/index.ts
  • packages/cli/src/commands/status/__tests__/status.test.ts
  • packages/cli/src/commands/status/quest.ts
  • packages/cli/src/config/index.ts
  • packages/cli/src/index.ts
  • packages/cli/src/installer/grants.ts
  • packages/cli/src/installer/index.ts
  • packages/cli/src/sql/cipherstash-encrypt-no-operator-family.sql
  • packages/cli/src/sql/cipherstash-encrypt-supabase.sql
  • packages/cli/src/sql/cipherstash-encrypt.sql
  • packages/cli/tests/e2e/command-help.e2e.test.ts
  • packages/cli/tests/e2e/smoke.e2e.test.ts
  • packages/cli/tests/e2e/v2-retirement.e2e.test.ts
  • packages/cli/tsconfig.scaffold.json
  • packages/cli/tsup.config.ts
  • packages/cli/vitest.config.ts
  • packages/migrate/README.md
  • packages/migrate/package.json
  • packages/migrate/src/__tests__/backfill-v3.integration.test.ts
  • packages/migrate/src/__tests__/manifest.test.ts
  • packages/migrate/src/__tests__/v2-retirement.test.ts
  • packages/migrate/src/__tests__/version.test.ts
  • packages/migrate/src/backfill.ts
  • packages/migrate/src/cursor.ts
  • packages/migrate/src/eql.ts
  • packages/migrate/src/index.ts
  • packages/migrate/src/install.ts
  • packages/migrate/src/manifest.ts
  • packages/migrate/src/state.ts
  • packages/migrate/src/version.ts
  • packages/migrate/tsconfig.json
  • packages/nextjs/__tests__/nextjs.test.ts
  • packages/nextjs/package.json
  • packages/nextjs/tsconfig.json
  • packages/prisma-next/src/stack/from-stack-v3.ts
  • packages/protect-dynamodb/.npmignore
  • packages/protect-dynamodb/CHANGELOG.md
  • packages/protect-dynamodb/README.md
  • packages/protect-dynamodb/__tests__/audit.test.ts
  • packages/protect-dynamodb/__tests__/dynamodb.test.ts
  • packages/protect-dynamodb/__tests__/error-codes.test.ts
  • packages/protect-dynamodb/__tests__/helpers.test.ts
  • packages/protect-dynamodb/package.json
  • packages/protect-dynamodb/src/helpers.ts
  • packages/protect-dynamodb/src/index.ts
  • packages/protect-dynamodb/src/operations/base-operation.ts
  • packages/protect-dynamodb/src/operations/bulk-decrypt-models.ts
  • packages/protect-dynamodb/src/operations/bulk-encrypt-models.ts
  • packages/protect-dynamodb/src/operations/decrypt-model.ts
  • packages/protect-dynamodb/src/operations/encrypt-model.ts
  • packages/protect-dynamodb/src/operations/search-terms.ts
  • packages/protect-dynamodb/src/types.ts
  • packages/protect-dynamodb/tsconfig.json
  • packages/protect-dynamodb/tsup.config.ts
  • packages/protect/.npmignore
  • packages/protect/CHANGELOG.md
  • packages/protect/README.md
  • packages/protect/__tests__/audit.test.ts
  • packages/protect/__tests__/basic-protect.test.ts
  • packages/protect/__tests__/bulk-protect.test.ts
  • packages/protect/__tests__/deprecated/search-terms.test.ts
  • packages/protect/__tests__/encrypt-query-searchable-json.test.ts
  • packages/protect/__tests__/encrypt-query-stevec.test.ts
  • packages/protect/__tests__/encrypt-query.test.ts
  • packages/protect/__tests__/error-codes.test.ts
  • packages/protect/__tests__/fixtures/index.ts
  • packages/protect/__tests__/helpers.test.ts
  • packages/protect/__tests__/infer-index-type.test.ts
  • packages/protect/__tests__/json-protect.test.ts
  • packages/protect/__tests__/jsonb-helpers.test.ts
  • packages/protect/__tests__/k-discriminator.test.ts
  • packages/protect/__tests__/keysets.test.ts
  • packages/protect/__tests__/lock-context.test.ts
  • packages/protect/__tests__/nested-models.test.ts
  • packages/protect/__tests__/number-protect.test.ts
  • packages/protect/__tests__/protect-ops.test.ts
  • packages/protect/__tests__/searchable-json-pg.test.ts
  • packages/protect/__tests__/supabase.test.ts
  • packages/protect/package.json
  • packages/protect/src/bin/runner.ts
  • packages/protect/src/bin/stash.ts
  • packages/protect/src/client.ts
  • packages/protect/src/ffi/helpers/error-code.ts
  • packages/protect/src/ffi/helpers/infer-index-type.ts
  • packages/protect/src/ffi/helpers/type-guards.ts
  • packages/protect/src/ffi/helpers/validation.ts
  • packages/protect/src/ffi/index.ts
  • packages/protect/src/ffi/model-helpers.ts
  • packages/protect/src/ffi/operations/base-operation.ts
  • packages/protect/src/ffi/operations/batch-encrypt-query.ts
  • packages/protect/src/ffi/operations/bulk-decrypt-models.ts
  • packages/protect/src/ffi/operations/bulk-decrypt.ts
  • packages/protect/src/ffi/operations/bulk-encrypt-models.ts
  • packages/protect/src/ffi/operations/bulk-encrypt.ts
  • packages/protect/src/ffi/operations/decrypt-model.ts
  • packages/protect/src/ffi/operations/decrypt.ts
  • packages/protect/src/ffi/operations/deprecated/search-terms.ts
  • packages/protect/src/ffi/operations/encrypt-model.ts
  • packages/protect/src/ffi/operations/encrypt-query.ts
  • packages/protect/src/ffi/operations/encrypt.ts
  • packages/protect/src/helpers/index.ts
  • packages/protect/src/helpers/jsonb.ts
  • packages/protect/src/identify/index.ts
  • packages/protect/src/index.ts
  • packages/protect/src/stash/index.ts
  • packages/protect/src/types.ts
  • packages/protect/tsconfig.json
  • packages/protect/tsup.config.ts
  • packages/schema/.npmignore
  • packages/schema/CHANGELOG.md
  • packages/schema/README.md
  • packages/schema/__tests__/schema.test.ts
  • packages/schema/__tests__/searchable-json.test.ts
  • packages/schema/package.json
  • packages/schema/src/index.ts
  • packages/schema/tsconfig.json
  • packages/schema/tsup.config.ts
  • packages/stack-drizzle/README.md
  • packages/stack-drizzle/__tests__/bigint.test.ts
  • packages/stack-drizzle/__tests__/codec.test.ts
  • packages/stack-drizzle/__tests__/column.test.ts
  • packages/stack-drizzle/__tests__/drizzle-operators-bigint.test.ts
  • packages/stack-drizzle/__tests__/drizzle-operators-jsonb.test.ts
  • packages/stack-drizzle/__tests__/exports.test.ts
  • packages/stack-drizzle/__tests__/indexes.test.ts
  • packages/stack-drizzle/__tests__/operators.test-d.ts
  • packages/stack-drizzle/__tests__/operators.test.ts
  • packages/stack-drizzle/__tests__/schema-extraction.test.ts
  • packages/stack-drizzle/__tests__/selector.test.ts
  • packages/stack-drizzle/__tests__/sql-dialect.test.ts
  • packages/stack-drizzle/__tests__/types.test-d.ts
  • packages/stack-drizzle/__tests__/types.test.ts
  • packages/stack-drizzle/integration/adapter.ts
  • packages/stack-drizzle/integration/json-adapter.ts
  • packages/stack-drizzle/integration/lock-context.integration.test.ts
  • packages/stack-drizzle/integration/null-persistence.integration.test.ts
  • packages/stack-drizzle/integration/relational.integration.test.ts
  • packages/stack-drizzle/package.json
  • packages/stack-drizzle/src/codec.ts
  • packages/stack-drizzle/src/column.ts
  • packages/stack-drizzle/src/index.ts
  • packages/stack-drizzle/src/indexes.ts
  • packages/stack-drizzle/src/operators.ts
  • packages/stack-drizzle/src/schema-extraction.ts
  • packages/stack-drizzle/src/sql-dialect.ts
  • packages/stack-drizzle/src/types.ts
  • packages/stack-drizzle/src/v3/index.ts
  • packages/stack-drizzle/src/v3/operators.ts
  • packages/stack-drizzle/src/v3/schema-extraction.ts
  • packages/stack-drizzle/tsconfig.typecheck.json
  • packages/stack-drizzle/tsup.config.ts
  • packages/stack-supabase/README.md
  • packages/stack-supabase/__tests__/column-map-predicate.test.ts
  • packages/stack-supabase/__tests__/helpers/supabase-mock.ts
  • packages/stack-supabase/__tests__/like-pattern.test.ts
  • packages/stack-supabase/__tests__/supabase-encryption-error.test.ts
  • packages/stack-supabase/__tests__/supabase-helpers.test.ts
  • packages/stack-supabase/__tests__/supabase-schema-builder.test.ts
  • packages/stack-supabase/__tests__/supabase-v3-builder.test.ts
  • packages/stack-supabase/__tests__/supabase-v3-factory.test.ts
  • packages/stack-supabase/__tests__/supabase-v3-json.test.ts
  • packages/stack-supabase/__tests__/supabase-v3-matrix.test.ts
  • packages/stack-supabase/__tests__/supabase-v3-select-star.test.ts
  • packages/stack-supabase/__tests__/supabase-v3-wire.test.ts
  • packages/stack-supabase/__tests__/supabase-v3.test-d.ts
  • packages/stack-supabase/__tests__/supabase.test.ts
  • packages/stack-supabase/integration/wire.integration.test.ts
  • packages/stack-supabase/package.json
  • packages/stack-supabase/src/column-map.ts
  • packages/stack-supabase/src/helpers.ts
  • packages/stack-supabase/src/index.ts
  • packages/stack-supabase/src/introspect.ts
  • packages/stack-supabase/src/like-pattern.ts
  • packages/stack-supabase/src/query-builder-v3.ts
  • packages/stack-supabase/src/query-builder.ts
  • packages/stack-supabase/src/query-dbspace.ts
  • packages/stack-supabase/src/query-encrypt.ts
  • packages/stack-supabase/src/query-filters.ts
  • packages/stack-supabase/src/query-mutation.ts
  • packages/stack-supabase/src/query-results.ts
  • packages/stack-supabase/src/types.ts
  • packages/stack/README.md
  • packages/stack/__tests__/audit.test.ts
  • packages/stack/__tests__/backward-compat.test.ts
  • packages/stack/__tests__/basic-protect.test.ts
  • packages/stack/__tests__/bulk-protect.test.ts
  • packages/stack/__tests__/decrypt-audit-forwarding.test.ts
  • packages/stack/__tests__/dynamodb/client-compat.test-d.ts
  • packages/stack/__tests__/dynamodb/construct-guard.test.ts
  • packages/stack/__tests__/dynamodb/encrypted-dynamodb-v3.test.ts
  • packages/stack/__tests__/dynamodb/resolve-decrypt.test.ts
  • packages/stack/__tests__/dynamodb/v2-table-forwarding.test.ts
  • packages/stack/__tests__/empty-schemas-boundary.test.ts
  • packages/stack/__tests__/encrypt-lock-context-guards.test.ts
  • packages/stack/__tests__/encrypt-query-searchable-json.test.ts
  • packages/stack/__tests__/encrypt-query-stevec.test.ts
  • packages/stack/__tests__/encryption-overloads.test-d.ts
  • packages/stack/__tests__/helpers/process-free-realm.mjs
  • packages/stack/__tests__/init-strategy.test.ts
  • packages/stack/__tests__/json-protect.test.ts
  • packages/stack/__tests__/lock-context-wiring.test.ts
  • packages/stack/__tests__/logger-edge-safety.test.ts
  • packages/stack/__tests__/number-protect.test.ts
  • packages/stack/__tests__/protect-ops.test.ts
  • packages/stack/__tests__/resolve-eql-version.test.ts
  • packages/stack/__tests__/typed-client-init-wire-version.test.ts
  • packages/stack/__tests__/typed-client-nominal-parity.test.ts
  • packages/stack/__tests__/typed-client-v3.test-d.ts
  • packages/stack/__tests__/typed-client-v3.test.ts
  • packages/stack/__tests__/v3-matrix/matrix-audit.test-d.ts
  • packages/stack/__tests__/v3-matrix/matrix-lock-context.test.ts
  • packages/stack/__tests__/wasm-inline-models.test.ts
  • packages/stack/dist-types/README.md
  • packages/stack/dist-types/encrypt-query.ts
  • packages/stack/dist-types/node16/encrypt-query.cts
  • packages/stack/dist-types/node16/encrypt-query.mts
  • packages/stack/dist-types/node16/tsconfig.json
  • packages/stack/dist-types/node16/wasm-inline.mts
  • packages/stack/dist-types/tsconfig.json
  • packages/stack/integration/identity/matrix-identity.integration.test.ts
  • packages/stack/integration/shared/harness.integration.test.ts
  • packages/stack/integration/shared/json-crypto.integration.test.ts
  • packages/stack/integration/shared/match-bloom.integration.test.ts
  • packages/stack/integration/shared/matrix-bulk.integration.test.ts
  • packages/stack/integration/shared/matrix-crypto.integration.test.ts
  • packages/stack/integration/shared/matrix-sql.integration.test.ts
  • packages/stack/integration/shared/schema-pg.integration.test.ts
  • packages/stack/integration/shared/schema-v3-client.integration.test.ts
  • packages/stack/integration/shared/v2-decrypt-compat.integration.test.ts
  • packages/stack/package.json
  • packages/stack/src/adapter-kit.ts
  • packages/stack/src/dynamodb/helpers.ts
  • packages/stack/src/dynamodb/index.ts
  • packages/stack/src/dynamodb/operations/bulk-decrypt-models.ts
  • packages/stack/src/dynamodb/operations/bulk-encrypt-models.ts
  • packages/stack/src/dynamodb/operations/decrypt-model.ts
  • packages/stack/src/dynamodb/operations/encrypt-model.ts
  • packages/stack/src/dynamodb/types.ts
  • packages/stack/src/encryption/helpers/index.ts
  • packages/stack/src/encryption/index.ts
  • packages/stack/src/encryption/operations/mapped-decrypt.ts
  • packages/stack/src/encryption/v3.ts
  • packages/stack/src/eql/v3/columns.ts
  • packages/stack/src/eql/v3/date-reconstruction.ts
  • packages/stack/src/schema/index.ts
  • packages/stack/src/types-public.ts
  • packages/stack/src/types.ts
  • packages/stack/src/utils/logger/index.ts
  • packages/stack/src/wasm-inline.ts
  • packages/stack/tsconfig.json
  • packages/stack/turbo.json
  • packages/test-kit/package.json
  • packages/test-kit/src/__tests__/install.test.ts
  • packages/test-kit/src/catalog.ts
  • packages/test-kit/src/install.ts
  • packages/wizard/package.json
  • packages/wizard/src/__tests__/post-agent.test.ts
  • packages/wizard/src/__tests__/rewrite-migrations.test.ts
  • packages/wizard/src/lib/gather.ts
  • packages/wizard/src/lib/install-skills.ts
  • packages/wizard/src/lib/post-agent.ts
  • packages/wizard/src/lib/rewrite-migrations.ts
  • packages/wizard/src/tools/wizard-tools.ts
  • packages/wizard/tsconfig.json
  • scripts/__tests__/bench-index-expressions.test.mjs
  • scripts/__tests__/cli-vitest-alias.test.mjs
  • scripts/__tests__/fixtures/lint-no-dead-package-paths/dead-ref.md
  • scripts/__tests__/fixtures/lint-no-dead-package-paths/dead-ref.yml
  • scripts/__tests__/fixtures/lint-no-dead-package-paths/dead-shell.md
  • scripts/__tests__/fixtures/lint-no-dead-package-paths/dir-with-changelog/CHANGELOG.md
  • scripts/__tests__/fixtures/lint-no-dead-package-paths/dir/nested.md
  • scripts/__tests__/fixtures/lint-no-dead-package-paths/globs.md
  • scripts/__tests__/fixtures/lint-no-dead-package-paths/live-refs.md
  • scripts/__tests__/fixtures/lint-no-dead-package-paths/many-dead-refs.md
  • scripts/__tests__/fixtures/lint-no-dead-package-paths/prefix.md
  • scripts/__tests__/fixtures/lint-no-dead-package-paths/sentence-final.md
  • scripts/__tests__/fixtures/lint-no-dead-package-paths/untracked-package.md
  • scripts/__tests__/fixtures/lint-no-dead-package-paths/uppercase.md
  • scripts/__tests__/fixtures/lint-typecheck-scope/excludes-dist/package.json
  • scripts/__tests__/fixtures/lint-typecheck-scope/excludes-dist/tsconfig.json
  • scripts/__tests__/fixtures/lint-typecheck-scope/has-include/package.json
  • scripts/__tests__/fixtures/lint-typecheck-scope/has-include/tsconfig.json
  • scripts/__tests__/fixtures/lint-typecheck-scope/jsonc-paths/package.json
  • scripts/__tests__/fixtures/lint-typecheck-scope/jsonc-paths/tsconfig.json
  • scripts/__tests__/fixtures/lint-typecheck-scope/no-gate/package.json
  • scripts/__tests__/fixtures/lint-typecheck-scope/no-gate/tsconfig.json
  • scripts/__tests__/fixtures/lint-typecheck-scope/unscoped-too/package.json
  • scripts/__tests__/fixtures/lint-typecheck-scope/unscoped-too/tsconfig.json
  • scripts/__tests__/fixtures/lint-typecheck-scope/unscoped/package.json
  • scripts/__tests__/fixtures/lint-typecheck-scope/unscoped/tsconfig.json
  • scripts/__tests__/lib/read-jsonc.mjs
  • scripts/__tests__/lint-no-dead-package-paths.test.mjs
  • scripts/__tests__/lint-no-hardcoded-runners.test.mjs
  • scripts/__tests__/lint-typecheck-scope.test.mjs
  • scripts/__tests__/no-removed-drizzle-surface.test.mjs
  • scripts/__tests__/no-removed-eql-version-flag.test.mjs
  • scripts/__tests__/rewriter-copies-in-sync.test.mjs
  • scripts/__tests__/turbo-skills-inputs.test.mjs
  • scripts/__tests__/vitest-config.test.mjs
  • scripts/__tests__/vitest-shared-alias.test.mjs
  • scripts/__tests__/workflow-turbo-build-deps.test.mjs
  • scripts/lint-no-dead-package-paths.mjs
  • scripts/lint-no-hardcoded-runners.mjs
  • scripts/lint-typecheck-scope.mjs
  • scripts/vitest.config.mjs
  • skills/stash-cli/SKILL.md
  • skills/stash-drizzle/SKILL.md
  • skills/stash-dynamodb/SKILL.md
  • skills/stash-edge/SKILL.md
  • skills/stash-encryption/SKILL.md
  • skills/stash-indexing/SKILL.md
  • skills/stash-postgres/SKILL.md
  • skills/stash-prisma-next/SKILL.md
  • skills/stash-supabase/SKILL.md
  • turbo.json
  • vitest.shared.ts

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch remove-v2

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderdan

Copy link
Copy Markdown
Contributor

Code review of #780's content, retargeted here

This review ran against fix/remove-v2-review-remediation before it merged. Since #780 landed first, the findings are retargeted to this umbrella — all of them are now in remove-v2 at 972a8a85. Every finding below was re-verified against that merged commit with a clean packages/stack rebuild, because the original pass used dist/ artefacts built on another branch.

One finding changed materially on re-verification — see the correction under Type surface below. Two candidates were dropped before this list as pre-existing on main and untouched by the diff (not(col,'like') bypassing the likeNeedle shim; the core client's Record<string, unknown> constraint rejecting interface row types — the latter fixed only for stack-supabase by 3c33c127).

The headline is a cluster, not a list

Findings 1–4 are all rewrite-migrations.ts, and they all end the same way: a commented-out or already-encrypted ALTER is rewritten into a live DROP COLUMN. This branch already hand-applied fixes to that rewriter four times (7eb47ed9, ae2fedd8, 05b7bf07, 383d3c2b) — twice each, because the 545-line file is duplicated between packages/wizard and packages/cli. That duplication is arguably the root finding: it's the reason partial fixes keep landing. Every fix below needs applying in both copies.

1. Quote-parity drift makes a commented ALTER execute as a DROPwizard/src/lib/rewrite-migrations.ts:215 (mirror cli/src/commands/db/rewrite-migrations.ts:207)

endOfQuoted treats only '' as an escape. A Postgres E'..\'..' string, or a dollar-quoted body with an odd apostrophe count, shifts quote parity for the rest of the file, so isInsideCommentOrString reports a commented-out ALTER as live.

Reproduced end-to-end against the real rewriteEncryptedAlterColumns:

CREATE FUNCTION note() RETURNS text AS $$ don't $$ LANGUAGE sql;
--> statement-breakpoint
-- it's ALTER TABLE "users" ALTER COLUMN "email" SET DATA TYPE eql_v3_text_search;

Result: rewritten=1, skipped=0, and the file now contains a bare ALTER TABLE "users" DROP COLUMN "email"; between two breakpoint markers — drizzle-kit runs it as its own chunk and destroys the column. A second repro with INSERT INTO t VALUES (E'a\'b', 'c'); before the commented ALTER does the same. The 137-test wizard suite is green on both.

2. CREATE_TABLE_RE is comment- and literal-blind — rewrite-migrations.ts:251 (mirror :243)

The lazy body ([\s\S]*?)\)\s*; stops at the first );, including one inside a -- comment or a DEFAULT string, so every encrypted column after it is missed by indexEncryptedColumns.

CREATE TABLE "users" (
  "id" text, -- pk (uuid);
  "email" eql_v3_text_search
);

captures only "id" text, -- pk (uuid. A later ALTER … SET DATA TYPE eql_v3_text_eq; then fails the already-encrypted check and is rewritten into ADD+DROP+RENAME — dropping a column full of ciphertext with no plaintext anywhere to backfill from, the exact irrecoverable outcome the function's own docblock warns about.

3. Chained SET DATA TYPE ALTERs are both rewritten — rewrite-migrations.ts:298 (mirror :290)

indexEncryptedColumns indexes CREATE TABLE, ADD COLUMN and RENAME, but never the strict matcher's own target: ALTER … SET DATA TYPE <encrypted domain>. Corpus 0001 CREATE TABLE "users" ("email" text); / 0002 ALTER … SET DATA TYPE eql_v2_encrypted; / 0003 … SET DATA TYPE eql_v3_text_search; — realistic for a directory where an older stack version generated 0002 before the sweep existed — returns rewritten=['0002','0003'], skipped=[]. After 0002 the column holds v2 ciphertext; 0003 then drops it, unflagged. Indexing the strict matcher's target (or re-indexing after each file) closes it.

4. columnKey is schema-sensitive but Postgres is not — rewrite-migrations.ts:275 (mirror :267)

JSON.stringify([schema ?? '', table, column]) keys "users" and "public"."users" differently. A column created unqualified (the drizzle-kit default) and later altered schema-qualified — which this diff's widened TABLE_REF now matches — misses the new already-encrypted guard and gets ADD+DROP+RENAME applied to a ciphertext column. The existing test scopes the check to the table varies the table name but never the qualification.

5. The sweep widened the blast radius, and writes land before the prompt — wizard/src/lib/post-agent.ts:163

rewriteEncryptedMigrations changed from "rewrite the first candidate directory that exists, then return" to sweepMigrationDirs(cwd, ['drizzle','migrations','src/db/migrations']). The docblock justifies this only against double-transformation ("the rewrite is idempotent"), but the dropped early return also bounded the blast radius to one directory. migrations/ and src/db/migrations/ are generic names owned by Knex, node-pg-migrate, Flyway, raw psql. In a project whose drizzle-kit out is drizzle/ but which also keeps a hand-maintained migrations/, stash wizard now rewrites any ALTER TABLE … SET DATA TYPE <eql domain>; there into the destructive sequence — in a directory it was never pointed at, with no dry run. writeFile commits at line 83; the confirm is at line 112.

Correctness

6. stash init scaffolds a file that does not compile — cli/src/commands/init/utils.ts:434 and :488

Both placeholders emit export const encryptionClient = await Encryption({ schemas: [] }), but this branch tightened both Encryption overloads to require a non-empty schema tuple. Re-verified against a fresh packages/stack build on 972a8a85:

TS2769: No overload matches this call.
  Overload 1 of 2 … Type '[]' is not assignable to type 'readonly [AnyV3Table, ...AnyV3Table[]]'.
    Source has 0 element(s) but target requires 1.

The old scaffold used EncryptionV3, whose readonly AnyV3Table[] bound accepted []. So every stash init now leaves a project whose first tsc / next build fails, in a file the CLI just wrote and told the user not to hand-edit. encryption-overloads.test-d.ts:49 asserts this call is a type error on purpose; the codegen tests only string-match the template, so nothing connects the two.

7. encrypt cutover picks an unrelated v3 column on a mixed table and exits 0 — cli/src/commands/encrypt/cutover.ts:97

Removing v2 from classifyEqlDomain drops v2 columns out of listEncryptedColumns, so pickEncryptedColumn falls through to the via: 'sole' rule — and cutover branches on info?.version === 3 without ever checking info.via. stash encrypt cutover --table users --column ssn, where ssn/ssn_encrypted are v2 at phase backfilled and the table also carries one v3 column email_enc: the <col>_encrypted convention no longer matches, others.length === 1 picks email_enc, and the command prints "Cut-over is not applicable to EQL v3 columns … point your application at email_enc" and returns 0. The v2 rename never runs and a scripted rollout reads success. drop.ts:106 does gate on via === 'sole', but its prescribed remedy is unreachable — pickEncryptedColumn returns null for a hint that isn't among the candidates, so --encrypted-column ssn_encrypted re-resolves via 'sole' and reprints the same error forever.

8. EncryptionV3 lost the invariant it existed to enforce — stack/src/encryption/index.ts:127

It is now a bare alias of Encryption. The old implementation forced config: { ...config, eqlVersion: 3 }; resolveEqlVersion returns explicit unchanged and never rejects explicit === 2 against an all-v3 schema set. A caller upgrading from EncryptionV3({ schemas: [v3Table], config: { eqlVersion: 2 } }) — previously auto-corrected and working — now builds a v2-wire FFI client over v3 concrete domains, the isV3Only && eqlVersion === 3 gate at line 947 also fails so they silently get the nominal client, and every subsequent encrypt dies inside protect-ffi with Cannot convert undefined or null to object — the opaque error the deleted override's comment says it existed to prevent. resolveEqlVersion already throws for mixed sets and for v2 SteVec; this is the one case it lets through.

9. Encryption only accepts an inline array literal — stack/src/encryption/index.ts:947 (corrected on re-verification)

The original finding claimed a silent type/runtime divergence — that a non-tuple schema list resolves to overload 2 and is typed EncryptionClient while the runtime returns typedClient's object. That is not what happens, and I'm correcting it here. What actually happens is a hard compile error on both overloads:

const schemas: AnyV3Table[] = [t]
await Encryption({ schemas })
// TS2769: No overload matches this call.
//   Overload 1 … Type 'AnyV3Table[]' is not assignable to 'readonly [AnyV3Table, ...AnyV3Table[]]'
//   Overload 2 … Type 'AnyV3Table[]' is not assignable to 'AtLeastOneCsTable<BuildableTable>'

Hoisting the config is enough to trigger it — const cfg = { schemas: [t] }; await Encryption(cfg) fails identically, because schemas widens to an array type. Only an inline literal at the call site compiles. The runtime accepts every one of these forms (schemas.every(hasBuildColumnKeyMap)), so this is a pure type-surface regression, and it's why stack-supabase casts through Parameters<typeof Encryption>[0]['schemas']. Less alarming than originally written, but it rejects a very ordinary way to build config, and it's the same Encryption-signature defect this PR's own Not in this PR section defers.

10. The table-less decrypt branch breaks the wasm-inline client — stack/src/dynamodb/operations/decrypt-model.ts:57 (same at bulk-decrypt-models.ts:61)

isV3Table(this.table) ? client.decryptModel(x, table) : client.decryptModel(x) assumes every client accepts a table-less decrypt, but WasmEncryptionClient.decryptModel(model, table) (wasm-inline.ts:1148) requires the table and returns a bare promise with no .audit(). A @cipherstash/stack/wasm-inline client — the documented path for Deno / Workers / Supabase Edge — structurally satisfies DynamoDBEncryptionClient, so encryptedDynamoDB({ encryptionClient }) accepts it. Reading a legacy v2 item takes the else-branch with table === undefined. The same commit's rewritten comments in helpers.ts:105 and types.ts ("Every client this package ships carries .audit() on decrypt") are false for that client, and the debug message it prints describes exactly what the user already did.

11. .or() corrupts nested and(…) / or(…) groups — stack-supabase/src/query-filters.ts:341

The rebuild path is taken whenever any condition names an encrypted column, but parseOrString/rebuildOrString cannot round-trip a nested group — the verbatim fallback's own comment admits this. Reproduced against the real helpers: parseOrString('and(age.gte.18,age.lte.30),email.eq.a@b.com') yields [{column:'and(age', op:'gte', value:'18,age.lte.30)'}, …], and rebuildOrString emits and(age.gte."18,age.lte.30)",email.eq."a@b.com" — unbalanced and(, group swallowed into a quoted operand, PGRST100. The identical call with only plaintext columns takes the verbatim branch and works, so adding one encrypted column to a working .or() breaks it. The 93-test supabase-v3-builder suite is green.

12. The bench seeds plaintext into encrypted columns — bench/src/harness/seed.ts:51

The v2→v3 port left the seed keying models by DB column name while v3 model operations key by JS property name. drizzle/setup.ts:20 declares encText: types.TextSearch('enc_text'); BenchPlaintextRow at :31 is keyed enc_text. So buildColumnKeyMap() is {encText:'enc_text', …}, no field is recognised as encrypted, the comment at seed.ts:66-68 ("returns rows keyed by the encryptedTable column names (snake_case here)") is now false, and r.enc_text at line 70 is undefined. Any credentialed run (test:local, bench:local, operators.explain.test.ts) then inserts raw plaintext into eql_v3_text_search / eql_v3_integer_ord / eql_v3_json_search — failing the domain CHECK, or storing plaintext in a column typed as encrypted. The new turbo run build --filter @cipherstash/bench gate is tsc --noEmit and passes because BenchPlaintextRow is hand-written and agrees with itself; the tests-bench CI job only runs the credential-free db-only filter, which never seeds.

Test coverage and shipped docs

13. ReturnType reads the last overload, making the M1 guard vacuous — stack-drizzle/__tests__/operators.test-d.ts:24

type V3Client = Awaited<ReturnType<typeof EncryptionV3>> now resolves to the nominal EncryptionClient, because EncryptionV3 = Encryption is overloaded — the exact trap this branch's own EncryptionClientFor docblock documents. Re-verified on 972a8a85: (c: V3Client) => c.init compiles, which it would not for the typed client. So "accepts the client EncryptionV3 returns with no cast" is now a byte-for-byte duplicate of the next test and the typed-client half of the contract is covered by nothing, while test:types stays green. The same annotation is a hard error in five files nothing type-checks — tsc in packages/stack-drizzle reports integration/adapter.ts(160,7): TS2739: Type 'TypedEncryptionClient<readonly [never]>' is missing … client, encryptConfig, init, plus identical errors at integration/json-adapter.ts:75 and integration/lock-context.integration.test.ts:158. The package has no typecheck script and its vitest typecheck scope is limited to *.test-d.ts.

14. The shipped stash-encryption skill now contradicts itself — skills/stash-encryption/SKILL.md:378

:378 ("return a plain Promise<Result<...>> (not a chainable operation)"), :686 ("so there is no .withLockContext() to chain") and :735 are contradicted by this branch's own MappedDecryptOperation — and by :1077 of the same file, which now says the opposite. Per AGENTS.md, these are copied verbatim into .claude/skills/ in every project by installSkills(), so the file steers agents away from the .audit() chain .changeset/stack-audit-on-decrypt.md advertises. packages/stack/README.md:710, :712, :717 and :531-532 publish the same stale return type inside the npm tarball, in a file this diff already edited.

15. The new required CI gate has both a false positive and a false negative — scripts/lint-no-dead-package-paths.mjs:43

PACKAGE_REF = /packages\/([a-z0-9][a-z0-9._-]*)/g greedily swallows sentence punctuation: The client lives in packages/stack. is reported as "packages/stack. does not exist" and the script exits 1 — so the next docs edit ending a sentence with a bare package path breaks the build with a message naming a live package. (The character class also excludes uppercase, so packages/Foo is never checked.) Conversely livePackages comes from readdirSync('packages') — the working tree, not tracked files — so packages/protect/src/ffi/index.ts, one of the references this script was commissioned to catch, is not flagged: packages/protect, packages/schema, packages/protect-dynamodb and packages/drizzle all survive as untracked dist/ + node_modules/ shells with 0 tracked files. Consequently pnpm run test:scripts is red on any checkout that has built main — 5 of its 10 self-tests fail. Related: this diff removes the dead packages/protect/src/bin/runner.ts allowlist entry from lint-no-hardcoded-runners.mjs:13 but leaves the equally dead packages/drizzle/src/bin/runner.ts directly beneath it, and scripts/ is not in the new linter's TARGETS.

Verified but cut for length

The ~98% duplication of the destructive rewriter across wizard and cli (root cause of 1–4); postprocessDecryptedRow rebuilding its key map per row, and its divergent second date-reconstruction implementation; DynamoDB v3 nested (dotted) date columns never being Date-reconstructed because buildColumnKeyMap is flat while toItemWithEqlPayloads rebuilds nested objects; or()'s structured branch dropping options.referencedTable; v2 columns backfilled from now on recording no eqlVersion; classifyEqlDomain no longer being able to return 2, making EncryptedColumnInfo.version and the isV3 checks vacuous; protectOps.eq (a nonexistent API) in the shipped implement prompt at setup-prompt.ts:283; dead v2-only bulkModelsToEncryptedPgComposites exports still on the audited adapter-kit seam; un-cleaned temp dirs and an unrestored sweepMigrationDirs mock in the new post-agent.test.ts; .changeset/dynamodb-eql-v3.md contradicted by same-release changesets; and the default @cipherstash/stack entry now statically importing the EQL v3 authoring DSL via encryption/index.ts -> ./v3.

tobyhede added a commit that referenced this pull request Jul 25, 2026
…icit schema

Two ways a corpus could still lose ciphertext to the ADD+DROP+RENAME, both
found by the #772 review. Neither is caught by the fail-closed `declared`
rule, because in both the column really is declared — as plaintext, by the
original CREATE TABLE.

Finding 3 — chained conversions. `indexColumnDeclarations` reads CREATE TABLE,
ADD COLUMN and RENAME, but never the strict matcher's own target. A corpus
carrying an earlier `SET DATA TYPE eql_v2_encrypted` (a stack version that
predates this sweep) left the column looking plaintext, so a later domain
change dropped its ciphertext. Record the conversion as the sweep walks, in
file-sorted order — not in the corpus-wide index, which has no order and would
flag the legitimate first conversion too.

Finding 4 — `"users"` and `"public"."users"` are the same table; Postgres
resolves the unqualified name through `search_path`. drizzle-kit emits
unqualified, while hand-written SQL and this sweep's own renderSafeAlter output
are qualified, so a corpus mixing the two split one column across two keys: the
already-encrypted guard missed, and the ALTER dropped ciphertext. Collapse the
implicit schema in `columnKey`. Only that one — `"app"."users"` stays distinct,
and there is no case folding, since TABLE_REF matches quoted identifiers only.

Both fixes land in both copies of the rewriter. 7 tests each: the two
destructive cases, the wrong-reason case finding 4 also produced
(`source-unknown` for a column declared two files up), and four guards for the
ways a naive fix breaks — the first conversion in a chain, a chain across
different columns, a commented-out earlier ALTER, and a real non-public schema.
tobyhede added a commit that referenced this pull request Jul 25, 2026
…ls CI

The rewriter lives twice — packages/wizard/src/lib and packages/cli/src/commands/db
— at ~99% similarity, and neither package depends on the other. Four #772 review
findings each needed the identical fix in both places; a fix landing in one still
passes that package's own suite, and this code emits DROP COLUMN.

Extraction is not available this cycle: packages/utils has no package.json,
@cipherstash/test-kit is private and build-less, and both bin entries set
skipNodeModulesBundle, so a shared workspace package would survive into dist as
an unresolvable bare specifier. That leaves publishing a new package into a
fixed mid-RC release group or adding noExternal to two CLI bundles — both worse
than the drift they would prevent, for now.

So compare instead. Everything outside an explicit `#region wizard-only` marker
must match byte-for-byte modulo comments, imports and the tool name in the
emitted header. Verified the guard actually fires: perturbing either copy fails
with a message naming both files and pointing at the region marker.
tobyhede added a commit that referenced this pull request Jul 26, 2026
The `unresolvedHint` fail-closed added for #772 finding 7 had no
`candidates.length > 0` guard, so it fired on pure EQL v2 tables too.

`encrypt backfill` records `encryptedColumn` in migrations.json
unconditionally, v2 included. On a pure-v2 table `listEncryptedColumns`
returns [] (the classifier recognises `eql_v3_*` only), so the hint failed
to resolve, `columnExists` found the real `eql_v2_encrypted` column, and
`cutover` / `drop` exited 1 — telling users to downgrade for a lifecycle
this same build still fully implements in cutover.ts / drop.ts.

Gate the fail-closed on a non-empty candidate list. Finding 7's protection
is unchanged: the mixed table it targets always has candidates. Order
`explainUnresolved`'s empty-candidates fall-through ahead of the hint
branch so both agree for direct callers.

Also drop the "this release no longer manages that lifecycle" claim from
the cutover/drop `via: 'sole'` messages — the build does still implement
it; the command simply resolves EQL v3 counterparts only.

Tests cover the previously untested shape: candidates [] + a recorded hint.

Review follow-ups:
- Extend the placeholder-table guard to `encrypt backfill` via
  loadEncryptionContext; correct the changeset/skill wording to name the
  commands that actually refuse (cutover/drop never read the client file).
- Alias `@cipherstash/migrate` to source in the CLI unit vitest config, so
  `pnpm --filter stash test` no longer needs a prior workspace build
  (verified: 888 tests pass with packages/migrate/dist removed).
- Revert the unrelated em-dash re-encoding in packages/cli/package.json.

@freshtonic freshtonic left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Reviewed the umbrella branch structurally, verified against source, and ran build/lint/unit tests in an isolated worktree (commit f7b8220).

Requesting changes — but to be clear up front: this means "not ready to merge as intended," not "the completed work is wrong." The landed work is genuinely strong (green build/lint/unit tests, a correctly preserved and tested v2 read path, no dangling references to the deleted packages). The reasons it can't merge are inherent to it being a draft umbrella branch mid-stack.

Blockers

  • Stated end-state unmet — v2 authoring/emission still present. packages/stack/src/index.ts:9 still re-exports the v2 DSL (encryptedColumn, encryptedField, encryptedTable), and resolveEqlVersion (encryption/index.ts:97-131) returns undefined for an all-v2 schema, which protect-ffi 0.30 defaults to wire v2 — so Encryption({ schemas: [v2Table] }) still emits v2 with no native-entry rejection (only wasm-inline.ts rejects). By design (PRs 9–12 outstanding), but it blocks merge of the umbrella.
  • CLI still carries the entire v2 surface (--eql-version 2, --latest, v2 SQL, v2 drizzle/supabase routing). PR 8 outstanding.
  • Branch is CONFLICTING, 9 commits behind main / 57 ahead (the body's "one commit behind" is stale). Needs rebase + conflict resolution (packages/stack-drizzle/README.md) before it can merge.
  • Phantom changeset bump.changeset/remove-eql-v2-packages.md bumps @cipherstash/nextjs: patch, but packages/nextjs depends only on jose and imports no @cipherstash/* package. It would ship a nextjs CHANGELOG entry about removing three packages it never used. Drop the line.

Major

  • Several unconsumed changesets (adapter-package-split.md, eql-v3-drizzle.md, and 6 others) advertise @cipherstash/stack-drizzle/v3 / @cipherstash/stack/eql/v3/drizzle as migration targets, but this PR removes the ./v3 subpath (a hard break). They render into the same 1.0.0-rc.5 CHANGELOG as remove-eql-v2-drizzle-root.md, producing directly contradictory guidance. Pre-existing on main, but this PR makes them false — reconcile before release.

Minor / nits

  • .changeset/remove-eql-v2-migrate-classifier.md is patch but the change makes listEncryptedColumns/resolveEncryptedColumn silently omit v2 columns under an unchanged signature — minor communicates it better (moot inside migrate's own major).
  • PR description is stale vs HEAD — several "Fix before merge" items are already resolved (root README swept to v3; packages/stack/README.md:446-451 now uses encryptedSupabase; drizzle-encrypted-indexes.md no longer advertises /v3). Refresh so reviewers/CHANGELOG readers aren't chasing fixed items.

What's done right (worth calling out)

  • v2 decrypt preservation is correct and tested: integration/shared/v2-decrypt-compat.integration.test.ts case #1c (a v3-configured client that never registered the v2 table decrypts v2 ciphertext — the actual customer invariant), plus a credential-free envelope-split path (23/23). The test documents its own fragility and instructs future PRs to keep #1c alive.
  • No dangling references to the deleted @cipherstash/protect / @cipherstash/schema / @cipherstash/protect-dynamodb in source/config.
  • Export-map integrity intact (every changed package keeps import + require; the only removal, stack-drizzle ./v3, is the intended documented hard break with sound reasoning).
  • Meta files kept honest per AGENTS.md (AGENTS.md, SECURITY.md, README.md, all seven skills); the stash-cli skill still accurately documents the v2 flags the not-yet-removed CLI ships, so skill/manifest stay consistent for the current state.

Caveat: I could not run the live ZeroKMS suite (no credentials), so the #1c v3-decrypts-v2 invariant is verified by inspection only — confirm it green in CI before merge.

tobyhede added a commit that referenced this pull request Jul 27, 2026
Follow-up to 1d14412, from an adversarial cross-check of that commit.

Blocking, and self-inflicted: `git checkout origin/main -- packages/cli/package.json`
(the em-dash revert) also imported main's Dependabot bump of posthog-node to
^5.41.0, which this branch's lockfile predates. Every CI job starts with
`pnpm install --frozen-lockfile`, so the PR died at step 1 before a test ran.
Back to ^5.40.0, matching the lockfile and packages/wizard.

resolve-eql's private `columnExists` used a bare `to_regclass($1)`. That form
parses and case-folds unquoted identifiers, so on a Prisma-style "User" table
the probe reported the column missing, the recorded pairing was treated as
stale, and the #772 fail-closed silently did not fire — falling through to the
sole/convention rules and resolving the guess it exists to prevent. migrate
already documents this exact anti-pattern (REGCLASS_SQL) and its test asserts
`not.toMatch(/to_regclass\(\$1\)/)`. Moved to a shared, case-exact
`columnExists` export and deleted the CLI copy; the test double is now
case-exact too, so it cannot hide a regression.

The placeholder guard read the harvested export map while the `db push` /
`db validate` guard it mirrors reads `getEncryptConfig().tables`. Those
disagree in both directions on one file: `schemas: [placeholderTable]` minus
the `export` keyword fell through to "table not found … Available: (none)" —
the error the guard replaces — and a stale placeholder export beside real
tables wrongly fired it. Now reads the same source.

cutover's `via:'sole'` refusal was nested inside `version === 3`; drop's is
top-level. Equivalent today, but cutover's v2 ladder does an irreversible
rename plus config promotion, so a restored v2 classification or a v4 family
would let cutover rename on a guess drop refuses. Hoisted to match.

Text that was false:
- the scaffold `stash init` writes into every customer project (and both
  fixtures) claimed `stash encrypt` commands refuse to run; only backfill does
- skills/stash-cli listed the client as loaded by `schema build` and `encrypt *`,
  and omitted `db push`
- skills/stash-cli and skills/stash-encryption still said cutover on a backfilled
  v3 column exits 0. Since `.cipherstash/` is gitignored, a clone or CI runner
  hits the new `sole` refusal on a pure-v3 table with an unconventional column
  name — the changeset's "pure-v3 unaffected" was wrong too
- both `sole` messages said "the table's only EQL column"; pickEncryptedColumn
  excludes the plaintext column first, so it fires with two
- the remedy said to drive the v2 lifecycle "directly"; there is no CLI route,
  so it now says to run the eql_v2 SQL yourself
- vitest.config.ts and packages/cli/AGENTS.md claimed the unit suite is
  self-contained. It is not: @cipherstash/stack is still reached via
  migrate/src/backfill.ts. The alias removed one of two couplings

typecheck:scaffold ran `tsc` directly, bypassing turbo, so it needed a prior
build and passed only by accident of step ordering behind steps that read as
independently droppable. Now a turbo task with dependsOn ^build.

Added scripts/__tests__/cli-vitest-alias.test.mjs — the CLI's alias map sits
outside the vitest.shared.ts guard and must, since stackSourceAlias's '@/'
would clobber the CLI's own. Runs in test:scripts, ahead of the build-dependent
suite.

Verified: frozen-lockfile install clean; 891 CLI unit, 41 migrate, 93 scripts,
76 pty e2e; code:check 0 errors; scaffold gate green through turbo with
packages/stack/dist absent.
tobyhede added a commit that referenced this pull request Jul 27, 2026
The `unresolvedHint` fail-closed added for #772 finding 7 had no
`candidates.length > 0` guard, so it fired on pure EQL v2 tables too.

`encrypt backfill` records `encryptedColumn` in migrations.json
unconditionally, v2 included. On a pure-v2 table `listEncryptedColumns`
returns [] (the classifier recognises `eql_v3_*` only), so the hint failed
to resolve, `columnExists` found the real `eql_v2_encrypted` column, and
`cutover` / `drop` exited 1 — telling users to downgrade for a lifecycle
this same build still fully implements in cutover.ts / drop.ts.

Gate the fail-closed on a non-empty candidate list. Finding 7's protection
is unchanged: the mixed table it targets always has candidates. Order
`explainUnresolved`'s empty-candidates fall-through ahead of the hint
branch so both agree for direct callers.

Also drop the "this release no longer manages that lifecycle" claim from
the cutover/drop `via: 'sole'` messages — the build does still implement
it; the command simply resolves EQL v3 counterparts only.

Tests cover the previously untested shape: candidates [] + a recorded hint.

Review follow-ups:
- Extend the placeholder-table guard to `encrypt backfill` via
  loadEncryptionContext; correct the changeset/skill wording to name the
  commands that actually refuse (cutover/drop never read the client file).
- Alias `@cipherstash/migrate` to source in the CLI unit vitest config, so
  `pnpm --filter stash test` no longer needs a prior workspace build
  (verified: 888 tests pass with packages/migrate/dist removed).
- Revert the unrelated em-dash re-encoding in packages/cli/package.json.
tobyhede added a commit that referenced this pull request Jul 27, 2026
Follow-up to 1d14412, from an adversarial cross-check of that commit.

Blocking, and self-inflicted: `git checkout origin/main -- packages/cli/package.json`
(the em-dash revert) also imported main's Dependabot bump of posthog-node to
^5.41.0, which this branch's lockfile predates. Every CI job starts with
`pnpm install --frozen-lockfile`, so the PR died at step 1 before a test ran.
Back to ^5.40.0, matching the lockfile and packages/wizard.

resolve-eql's private `columnExists` used a bare `to_regclass($1)`. That form
parses and case-folds unquoted identifiers, so on a Prisma-style "User" table
the probe reported the column missing, the recorded pairing was treated as
stale, and the #772 fail-closed silently did not fire — falling through to the
sole/convention rules and resolving the guess it exists to prevent. migrate
already documents this exact anti-pattern (REGCLASS_SQL) and its test asserts
`not.toMatch(/to_regclass\(\$1\)/)`. Moved to a shared, case-exact
`columnExists` export and deleted the CLI copy; the test double is now
case-exact too, so it cannot hide a regression.

The placeholder guard read the harvested export map while the `db push` /
`db validate` guard it mirrors reads `getEncryptConfig().tables`. Those
disagree in both directions on one file: `schemas: [placeholderTable]` minus
the `export` keyword fell through to "table not found … Available: (none)" —
the error the guard replaces — and a stale placeholder export beside real
tables wrongly fired it. Now reads the same source.

cutover's `via:'sole'` refusal was nested inside `version === 3`; drop's is
top-level. Equivalent today, but cutover's v2 ladder does an irreversible
rename plus config promotion, so a restored v2 classification or a v4 family
would let cutover rename on a guess drop refuses. Hoisted to match.

Text that was false:
- the scaffold `stash init` writes into every customer project (and both
  fixtures) claimed `stash encrypt` commands refuse to run; only backfill does
- skills/stash-cli listed the client as loaded by `schema build` and `encrypt *`,
  and omitted `db push`
- skills/stash-cli and skills/stash-encryption still said cutover on a backfilled
  v3 column exits 0. Since `.cipherstash/` is gitignored, a clone or CI runner
  hits the new `sole` refusal on a pure-v3 table with an unconventional column
  name — the changeset's "pure-v3 unaffected" was wrong too
- both `sole` messages said "the table's only EQL column"; pickEncryptedColumn
  excludes the plaintext column first, so it fires with two
- the remedy said to drive the v2 lifecycle "directly"; there is no CLI route,
  so it now says to run the eql_v2 SQL yourself
- vitest.config.ts and packages/cli/AGENTS.md claimed the unit suite is
  self-contained. It is not: @cipherstash/stack is still reached via
  migrate/src/backfill.ts. The alias removed one of two couplings

typecheck:scaffold ran `tsc` directly, bypassing turbo, so it needed a prior
build and passed only by accident of step ordering behind steps that read as
independently droppable. Now a turbo task with dependsOn ^build.

Added scripts/__tests__/cli-vitest-alias.test.mjs — the CLI's alias map sits
outside the vitest.shared.ts guard and must, since stackSourceAlias's '@/'
would clobber the CLI's own. Runs in test:scripts, ahead of the build-dependent
suite.

Verified: frozen-lockfile install clean; 891 CLI unit, 41 migrate, 93 scripts,
76 pty e2e; code:check 0 errors; scaffold gate green through turbo with
packages/stack/dist absent.
tobyhede added a commit that referenced this pull request Jul 27, 2026
fix(stack,bench): wasm-inline DynamoDB v2 reads, and a bench seed that never encrypted (#772 review findings 10, 12)
tobyhede added a commit that referenced this pull request Jul 27, 2026
The `unresolvedHint` fail-closed added for #772 finding 7 had no
`candidates.length > 0` guard, so it fired on pure EQL v2 tables too.

`encrypt backfill` records `encryptedColumn` in migrations.json
unconditionally, v2 included. On a pure-v2 table `listEncryptedColumns`
returns [] (the classifier recognises `eql_v3_*` only), so the hint failed
to resolve, `columnExists` found the real `eql_v2_encrypted` column, and
`cutover` / `drop` exited 1 — telling users to downgrade for a lifecycle
this same build still fully implements in cutover.ts / drop.ts.

Gate the fail-closed on a non-empty candidate list. Finding 7's protection
is unchanged: the mixed table it targets always has candidates. Order
`explainUnresolved`'s empty-candidates fall-through ahead of the hint
branch so both agree for direct callers.

Also drop the "this release no longer manages that lifecycle" claim from
the cutover/drop `via: 'sole'` messages — the build does still implement
it; the command simply resolves EQL v3 counterparts only.

Tests cover the previously untested shape: candidates [] + a recorded hint.

Review follow-ups:
- Extend the placeholder-table guard to `encrypt backfill` via
  loadEncryptionContext; correct the changeset/skill wording to name the
  commands that actually refuse (cutover/drop never read the client file).
- Alias `@cipherstash/migrate` to source in the CLI unit vitest config, so
  `pnpm --filter stash test` no longer needs a prior workspace build
  (verified: 888 tests pass with packages/migrate/dist removed).
- Revert the unrelated em-dash re-encoding in packages/cli/package.json.
tobyhede added a commit that referenced this pull request Jul 27, 2026
Follow-up to 1d14412, from an adversarial cross-check of that commit.

resolve-eql's private `columnExists` used a bare `to_regclass($1)`. That form
parses and case-folds unquoted identifiers, so on a Prisma-style "User" table
the probe reported the column missing, the recorded pairing was treated as
stale, and the #772 fail-closed silently did not fire — falling through to the
sole/convention rules and resolving the guess it exists to prevent. migrate
already documents this exact anti-pattern (REGCLASS_SQL) and its test asserts
`not.toMatch(/to_regclass\(\$1\)/)`. Moved to a shared, case-exact
`columnExists` export and deleted the CLI copy; the test double is now
case-exact too, so it cannot hide a regression.

The placeholder guard read the harvested export map while the `db push` /
`db validate` guard it mirrors reads `getEncryptConfig().tables`. Those
disagree in both directions on one file: `schemas: [placeholderTable]` minus
the `export` keyword fell through to "table not found … Available: (none)" —
the error the guard replaces — and a stale placeholder export beside real
tables wrongly fired it. Now reads the same source.

cutover's `via:'sole'` refusal was nested inside `version === 3`; drop's is
top-level. Equivalent today, but cutover's v2 ladder does an irreversible
rename plus config promotion, so a restored v2 classification or a v4 family
would let cutover rename on a guess drop refuses. Hoisted to match.

Text that was false:
- the scaffold `stash init` writes into every customer project (and both
  fixtures) claimed `stash encrypt` commands refuse to run; only backfill does
- skills/stash-cli listed the client as loaded by `schema build` and `encrypt *`,
  and omitted `db push`
- skills/stash-cli and skills/stash-encryption still said cutover on a backfilled
  v3 column exits 0. Since `.cipherstash/` is gitignored, a clone or CI runner
  hits the new `sole` refusal on a pure-v3 table with an unconventional column
  name — the changeset's "pure-v3 unaffected" was wrong too
- both `sole` messages said "the table's only EQL column"; pickEncryptedColumn
  excludes the plaintext column first, so it fires with two
- the remedy said to drive the v2 lifecycle "directly"; there is no CLI route,
  so it now says to run the eql_v2 SQL yourself
- vitest.config.ts and packages/cli/AGENTS.md claimed the unit suite is
  self-contained. It is not: @cipherstash/stack is still reached via
  migrate/src/backfill.ts. The alias removed one of two couplings

typecheck:scaffold ran `tsc` directly, bypassing turbo, so it needed a prior
build and passed only by accident of step ordering behind steps that read as
independently droppable. Now a turbo task with dependsOn ^build.

Added scripts/__tests__/cli-vitest-alias.test.mjs — the CLI's alias map sits
outside the vitest.shared.ts guard and must, since stackSourceAlias's '@/'
would clobber the CLI's own. Runs in test:scripts, ahead of the build-dependent
suite.

Verified: frozen-lockfile install clean; 891 CLI unit, 41 migrate, 93 scripts,
76 pty e2e; code:check 0 errors; scaffold gate green through turbo with
packages/stack/dist absent.
tobyhede added a commit that referenced this pull request Jul 27, 2026
fix(cli): mixed-table encrypt lifecycle, and a stash init scaffold that compiles (#772 review findings 6, 7)
tobyhede added a commit that referenced this pull request Jul 27, 2026
…tch their own rot

Follow-up to the #772 review findings, addressing what the review of that
review turned up.

`stash init`'s setup prompt named `createEncryptionOperators` unconditionally.
That symbol is exported by `@cipherstash/stack-drizzle` alone, so a Supabase or
Prisma Next project was sent after a package that is not in its dependency
tree — the previous `protectOps.eq` was wrong for everyone, this was wrong for
three integrations out of four. Step 5 now branches through
`queryOperatorGuidance()`, following the `migrationCommands()` pattern already
in the file: `ops.eq` for Drizzle, the `encryptedSupabase` wrapper's own
filters for Supabase, the `eql*` column operators for Prisma Next, and
`client.encryptQuery(...)` for plain Postgres — which is also pointed at
`stash-encryption`, since it is the default integration and installs no
integration skill, making "see the integration skill" a dangling pointer.

The package-path linter reported an untracked-but-present package as "does not
exist" — finding 15's false alarm pointed the other way, at a directory sitting
right there on disk. The live set now unions `git ls-files --others
--exclude-standard`. `--directory` is deliberately not passed: it collapses an
all-ignored directory to one entry and would resurrect exactly the `dist/`-and-
`node_modules/` shells the linter exists to catch (verified both ways). git
failing now exits 2 with an actionable message instead of a raw ENOENT stack
trace and exit 1, which was indistinguishable from a genuine lint failure, and
an empty live set refuses to run rather than flagging every reference at once.

The `scans scripts/ but not its fixtures` test asserted the repo was clean —
byte-for-byte what the suite's first test already asserted, and passing whether
or not `scripts` was in TARGETS. It now plants an offender in the scanned
directory. Verified by mutation: it dies when `scripts` is dropped, as the
untracked-package test dies without the union and the git-failure test dies
when the exit code is flipped.

The runners linter now requires each allowlist entry to exist and to still
contain an unexcused `npx` literal. It immediately found a second stale entry
the sibling linter structurally cannot see, since it only matches the
`packages/<name>` shape: `setup-prompt.ts` has had no `npx` since its switch
moved to `utils.ts`. Removed.

Also corrects two claims: the test comment saying `packages/drizzle` never
existed in git history (it was added speculatively by c671560, became
load-bearing 31 minutes later with 9d259e6, and rotted when 413ca39 deleted
the package), and the `AnyEncryptedTable` doc comment still promising v3 is
"purely additive and no existing caller has to change" — true of decrypt, false
of encrypt since the v2 write overloads were removed.
tobyhede added a commit that referenced this pull request Jul 27, 2026
…tch their own rot

Follow-up to the #772 review findings, addressing what the review of that
review turned up.

`stash init`'s setup prompt named `createEncryptionOperators` unconditionally.
That symbol is exported by `@cipherstash/stack-drizzle` alone, so a Supabase or
Prisma Next project was sent after a package that is not in its dependency
tree — the previous `protectOps.eq` was wrong for everyone, this was wrong for
three integrations out of four. Step 5 now branches through
`queryOperatorGuidance()`, following the `migrationCommands()` pattern already
in the file: `ops.eq` for Drizzle, the `encryptedSupabase` wrapper's own
filters for Supabase, the `eql*` column operators for Prisma Next, and
`client.encryptQuery(...)` for plain Postgres — which is also pointed at
`stash-encryption`, since it is the default integration and installs no
integration skill, making "see the integration skill" a dangling pointer.

The package-path linter reported an untracked-but-present package as "does not
exist" — finding 15's false alarm pointed the other way, at a directory sitting
right there on disk. The live set now unions `git ls-files --others
--exclude-standard`. `--directory` is deliberately not passed: it collapses an
all-ignored directory to one entry and would resurrect exactly the `dist/`-and-
`node_modules/` shells the linter exists to catch (verified both ways). git
failing now exits 2 with an actionable message instead of a raw ENOENT stack
trace and exit 1, which was indistinguishable from a genuine lint failure, and
an empty live set refuses to run rather than flagging every reference at once.

The `scans scripts/ but not its fixtures` test asserted the repo was clean —
byte-for-byte what the suite's first test already asserted, and passing whether
or not `scripts` was in TARGETS. It now plants an offender in the scanned
directory. Verified by mutation: it dies when `scripts` is dropped, as the
untracked-package test dies without the union and the git-failure test dies
when the exit code is flipped.

The runners linter now requires each allowlist entry to exist and to still
contain an unexcused `npx` literal. It immediately found a second stale entry
the sibling linter structurally cannot see, since it only matches the
`packages/<name>` shape: `setup-prompt.ts` has had no `npx` since its switch
moved to `utils.ts`. Removed.

Also corrects two claims: the test comment saying `packages/drizzle` never
existed in git history (it was added speculatively by c671560, became
load-bearing 31 minutes later with 9d259e6, and rotted when 413ca39 deleted
the package), and the `AnyEncryptedTable` doc comment still promising v3 is
"purely additive and no existing caller has to change" — true of decrypt, false
of encrypt since the v2 write overloads were removed.
tobyhede added a commit that referenced this pull request Jul 27, 2026
fix(skills,stack,scripts): shipped decrypt guidance, and a sound package-path linter (#772 review findings 14, 15)
tobyhede added a commit that referenced this pull request Jul 28, 2026
…icit schema

Two ways a corpus could still lose ciphertext to the ADD+DROP+RENAME, both
found by the #772 review. Neither is caught by the fail-closed `declared`
rule, because in both the column really is declared — as plaintext, by the
original CREATE TABLE.

Finding 3 — chained conversions. `indexColumnDeclarations` reads CREATE TABLE,
ADD COLUMN and RENAME, but never the strict matcher's own target. A corpus
carrying an earlier `SET DATA TYPE eql_v2_encrypted` (a stack version that
predates this sweep) left the column looking plaintext, so a later domain
change dropped its ciphertext. Record the conversion as the sweep walks, in
file-sorted order — not in the corpus-wide index, which has no order and would
flag the legitimate first conversion too.

Finding 4 — `"users"` and `"public"."users"` are the same table; Postgres
resolves the unqualified name through `search_path`. drizzle-kit emits
unqualified, while hand-written SQL and this sweep's own renderSafeAlter output
are qualified, so a corpus mixing the two split one column across two keys: the
already-encrypted guard missed, and the ALTER dropped ciphertext. Collapse the
implicit schema in `columnKey`. Only that one — `"app"."users"` stays distinct,
and there is no case folding, since TABLE_REF matches quoted identifiers only.

Both fixes land in both copies of the rewriter. 7 tests each: the two
destructive cases, the wrong-reason case finding 4 also produced
(`source-unknown` for a column declared two files up), and four guards for the
ways a naive fix breaks — the first conversion in a chain, a chain across
different columns, a commented-out earlier ALTER, and a real non-public schema.
@tobyhede

Copy link
Copy Markdown
Contributor Author

Thanks Dan — I audited both current-head reports, verified the findings against 3a6395c2, deduplicated overlaps, and checked the older review for unresolved carryovers.

The result is 16 distinct remaining items. I’ve consolidated them into the existing trackers:

  1. Invalid eql_v3_encrypted setup guidance — #816
  2. Changelog advertises removed adapter APIs and superseded behavior — #816
  3. Release contract contradicts retained public v2 emission — #815
  4. Adapter changesets target @cipherstash/stack instead of their owning packages — #816
  5. DynamoDB changeset incorrectly says typed clients lack decrypt audit — #816
  6. Unrelated @cipherstash/nextjs deletion changelog — #816
  7. Nested string .or() groups are corrupted when encrypted conditions trigger rebuilding — #816
  8. E2E typecheck relies on an ambient TypeScript installation — #816, related: #531
  9. Nullish matches() operands degrade to plaintext filtering — #816
  10. Removed legacy encryptedSupabase({...}) call shape lacks an actionable migration guard — #816
  11. .csv() results are passed to bulkDecryptModels#816, related: #638
  12. $tag$ inside an unquoted identifier opens a phantom dollar quote — #813, related: #811
  13. Escaped LIKE %/_ literals are treated as wildcards — #816
  14. Typed-client re-init retains stale schema-derived state — #815, related: #637
  15. Date reconstruction diverges across native, WASM, and Supabase paths — #816, related: #779
  16. CLI and wizard skill maps are duplicated and already divergent — #816

The issue bodies now contain explicit acceptance criteria for each item:

  • #815: core v3-only emission contract and typed-client re-init safety
  • #813: migration-rewriter safety and the identifier/dollar-quote lexer case
  • #816: adapter correctness, release metadata, CI, setup guidance, date behavior, and skill-map consistency

Open-PR audit:

  • #823 partially mitigates item 12 by removing generated destructive DROP/RENAME operations, but it does not fix the phantom-dollar-quote parsing case itself.
  • #825 touches adjacent setup and changeset files but does not resolve the invalid domain guidance or skill-map divergence.
  • No currently open PR fully resolves the remaining inventory.

The broader remediation remains tracked by #778 under the #707 removal umbrella.

@coderdan coderdan left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Approved based on follow-ups tracked in separate issues.

tobyhede added 3 commits July 29, 2026 12:05
`detectColumnEqlVersion` returns null both for a column whose domain
isn't `eql_v3_*` and for a column that isn't there at all, so backfill's
single error accused a user who had merely not added `<col>_encrypted`
yet of running legacy EQL v2 — and handed them a remedy (migrate the
domain) for a column that does not exist.

Extract the guard into `assertEqlV3Target` and probe `columnExists` on
the failure path only; a v3 answer already proves existence, so the
happy path still makes one catalog query.

Also mark the retired surface in two internal design plans
(`stash db push`, `stash encrypt cutover`, the `eql_v2.*` wrappers,
`commands/db/push.ts`) so a future reader doesn't chase files that were
deleted with EQL v2. The historical narrative is left intact.
tobyhede added 15 commits July 29, 2026 12:50
test(scripts): cover repository-linter race guards
…) recursion

Two guards in the string-form `.or()` path had no test that could see them
fail.

`substituteOrStringLeaves` sorts its replacements right-to-left so an earlier
source span stays valid after a later one grew — and every v3 envelope is far
longer than the plaintext operand it replaces. Sorting ascending instead
splices the second envelope INSIDE the first and leaks the trailing condition
verbatim: `email.eq."{\"created_at.gte."{…}"…}",createdAt.gte.2026-01-01`.
Only one existing test replaced two leaves at all (the `is`-on-an-encrypted-
column case, where the second replacement is `created_at.is.null` — shorter
than the span it fills), so the growth case that motivates the sort was
untested.

`parseOrStringAt`'s group regex admits a `not.` prefix. Drop it and
`not.and(email.eq.ada,…)` stops being a group: the leaf parser splits it at
the first dot into the pseudo-column `not`, which matches no encrypted
column, so the whole or-string takes the verbatim branch and `ada` goes to
PostgREST in the clear against an encrypted column.

Both were verified red against those exact mutations and green on restore.
… it explains

`reconstructDate` was deleted from `wasm-inline.ts` when date reconstruction
moved to `eql/v3/date-reconstruction.ts`, but its doc comment survived and
came to rest above `INTERNAL_CONSTRUCT` — two JSDoc blocks stacked over one
constant, the first documenting nothing.

The explanation it carried is still the reason `reconstructDateValue` guards
on `Number.isNaN` at all, so it lands there: an unparseable stored value must
come back as the raw value, not as an Invalid Date whose later
`.toISOString()` throws far from the column that produced it (#742 review).
…and correct the skill

`csv()` throws unconditionally — PostgREST serializes rows server-side, so a
CSV response carries ciphertext the adapter never gets to decrypt. Nothing can
therefore push `{ kind: 'csv' }`, yet the transform survived in three places:
the `TransformOp` union, the `toDbSpace` pass-through switch, and the
`buildAndExecuteQuery` apply switch. All three go; the fail-closed throw stays
exactly as it is.

`skills/stash-supabase/SKILL.md` still listed `.csv()` among the transforms
"passed through to Supabase directly". That ships inside the `stash` tarball
and gets copied into customer repos, so it was about to become wrong guidance
in someone else's codebase. It now documents the throw and shows serializing
the decrypted rows instead.
`postprocessDecryptedRow` called `reconstructDatePaths(out, [key])` once per
dotted date key, and that helper allocates a fresh shallow row copy on every
call — so a row with N nested date columns paid N clones to produce one
result. Collect the dotted keys and make a single call.

The batching is also the correctness-preserving form for two paths under a
shared prefix: inside one call the second path clones the intermediate the
first already rebuilt (`targetChild` wins over `sourceChild`), so both Dates
and their plaintext siblings survive. Covered by a new `profile.createdAt` /
`profile.updatedAt` test, which goes red if that preference is dropped.

The flat-key branch is unchanged.
…checks

`checkedInsertRows` reads as though it validates the rows it returns. It
validates `rowKey` and `testRunId` are strings and then does `return rows as
T[]` — the encrypted columns, the ones that could plausibly be malformed, are
never looked at.

Renamed to `assertScopeKeys`, with a doc comment saying plainly that it
re-establishes the Drizzle insert type after asserting only that the two
plaintext scope keys survived encryption.

Verified through `pnpm --filter @cipherstash/stack-drizzle run test:types`,
which is the script driving `tsconfig.typecheck.json` — now widened to
`integration/**/*.ts`. Confirmed the gate really compiles this file by
planting a deliberate type error in it and watching the run exit 1.
… declared

The parity assertion lived in the CLI's unit suite and reached the wizard
through `../../../../../../wizard/src/lib/install-skills.js` — a deep relative
path into a package `packages/cli` declares no dependency on, targeting a
non-published internal module. If the wizard relocated that file the only
signal was a module-not-found in a different package's test run.

Chose option (a), moving it to `e2e/`, over adding a devDependency and a
comment (option (b)), for three reasons:

- `e2e/` already declares BOTH `stash` and `@cipherstash/wizard` as workspace
  dependencies, so the layering is honest without widening the CLI's dep graph
  for a single assertion.
- `e2e` has a `typecheck` script over `tests/**` and CI runs it ("Typecheck
  (e2e)"). `packages/cli` has no typecheck script at all, so this is the only
  placement where a relocated module is a COMPILE error. Verified: pointing the
  import at a non-existent path fails `pnpm --filter @cipherstash/e2e run
  typecheck` with TS2307.
- Cross-package behaviour is what that workspace is for, per its own
  description — and reading source rather than a built binary is an existing
  idiom there (`package-managers.e2e.test.ts` Suite A), so no build is needed.

`SKILL_MAP` stays unexported from the wizard's package entry — the published
surface should not grow for a test.

The assertion also got stronger in the move: the wizard→CLI name mapping is now
a `Record<WizardIntegration, CliIntegration>`, so a new integration on either
side fails to compile or fails the key-coverage test, instead of being silently
omitted from a hand-listed set of four expectations.
…the nested-or() fix was

Two changesets had been edited to delete the statement that
`encryptedSupabaseV3` survives as a type-identical `@deprecated` alias. It
does — `packages/stack-supabase/src/index.ts:275`, alongside the `*V3` type
aliases at 282-297 — the repo's own tests import it, and `AGENTS.md`
documents it. A changeset that contradicts the shipped code is worse than a
terse one, so both get a tight sentence back.

`adapter-release-readiness` compressed the nested-group fix into "preserves
nested PostgREST boolean expressions", which reads as a formatting nicety. It
was a disclosure fix: with no group recursion, `and(createdAt.gte.…,…)` was
cut at the first dot into the pseudo-column `and(createdAt`, matched no
encrypted column, and took the verbatim branch — so the operand reached
PostgREST as plaintext against an encrypted column, under the JS property
name rather than the DB column name. Users need that stated outright.

Also records the `stash-supabase` skill correction (csv() throws, it is not a
pass-through transform) on the changeset that already bumps `stash`.
…mments

Widening `stack-drizzle`'s typecheck `include` to `integration/**/*.ts` made
the surrounding comments stale, and rewriting them dropped the two things that
made them actionable: the per-package error counts, and the `#778` root cause
(`V3_MATRIX`'s `indexes` union in `@cipherstash/test-kit`).

Those counts are the mechanism the original comment describes — "widen this
include as that count comes down" is not a usable instruction once the count is
gone, and "tracked separately" does not say where. Restore both, keeping the
corrected scope: drizzle now type-tests every `integration/**` source, so only
its `src` and runtime tests remain ungated.

Comment-only; no build or gate behaviour changes.
…oduction runs

`parseOrStringWithSpans` became the only parser any production path calls when
the nested-group fix landed, but `parseOrString` survived alongside it and kept
the entire helpers suite — including assertions that pinned the NON-recursing
behaviour, e.g. `and(a.eq.1,b.eq.2),c.eq.3` yielding two conditions, one of them
a junk leaf on a column literally named `and(a`. The parser with the coverage
was the dead one; the live one's group recursion and source spans were exercised
only indirectly, through the builder.

Delete the wrapper, repoint the suite at `parseOrStringWithSpans`, and drop the
now-constant `recurseGroups` parameter. A local `parseConditions` strips
`sourceSpan` so every existing `toEqual` stays exactly as strict about the
condition shape as it was.

Adds the span-level coverage that existed nowhere: a leaf one and two groups
deep, inside `not.and(`, after array and jsonb containment literals whose commas
are not delimiters, with surrounding whitespace excluded, and two identical
leaves getting distinct spans — plus `substituteOrStringLeaves` replacing leaves
at two nesting depths. Both are real regressions tests: flipping the splice to
left-to-right fails two of them, and shifting the group base offset by one fails
seven. Nothing in the suite caught either mutation before.
`parseLikeNeedle` decides which `like`/`ilike` patterns can be delegated to
encrypted fuzzy matching and what literal term the search receives, but its only
coverage was two end-to-end cases in the builder suite, which pin the wire
envelope rather than the parse. Its predecessor (`replace(/^%+/, '')` plus
`pattern.includes('_')`) got the escaping wrong in both directions, so the rules
are worth holding directly: 9 of the 22 new tests fail against that predecessor.

Pins in particular that `hasUnsupportedWildcard` tracks UNESCAPED wildcards only
— an escaped `\_` is a literal and must not reject the query — and that only
unescaped leading/trailing `%` are strippable, so a literal `\%` at either end
stops the strip instead of being eaten by it.

A trailing lone backslash stays a literal backslash rather than throwing;
Postgres rejects such a pattern outright, but this needle is an approximation
and the plaintext `like` path never reaches here. Documented as deliberate in
the JSDoc so it does not get "fixed" into a throw.

Also corrects the throw's parenthetical, which still read `an internal "%" or
any "_"` — escaped metacharacters are honoured now, so it names unescaped ones.
…ate-shape break

Three things this release changes that a reader could only find at runtime.

`csv()` throws on every encrypted query, but the interface member is still typed
`Self` with no `@deprecated` tag, so an existing call site got no editor or
compile signal and would discover the removal on its first request. Tag it; the
`Self` return type stays, since it is a deliberate choice to keep the chain
shape. `docs/reference/supabase-sdk.md` still advertised `.csv` among the
transforms passed through to Supabase — the bundled skill was corrected in this
PR, the reference was not.

Date reconstruction now returns the raw value when a stored value does not
parse. Before this branch the native typed client pushed every date-like column
through `new Date(...)` unconditionally, so an unparseable value came back as an
Invalid `Date`: `instanceof Date` held and `.getTime()` yielded `NaN`. It now
comes back as the raw string and that call throws a `TypeError`, while the
declared column type is still `Date`. The changeset said only "leaving invalid
values unchanged", which undersells it. Both `stash-encryption` and
`stash-drizzle` asserted flatly that `Date` columns are reconstructed to real
`Date` instances — Drizzle decrypts through the same typed client, so both now
carry the caveat.

Bumps `@cipherstash/stack-supabase` and `@cipherstash/stack` from patch to
minor. `csv()` and `matches(column, null)` throwing where they previously did
something are behavioural breaks, and the date-shape change originates in
`stack`; the migrate classifier changeset was raised for the same reason. The
emitted version is unchanged under rc prerelease mode with a fixed group — this
is signal, not version math.

Also names the parser that actually runs in the nested-group bullet, since
`parseOrString` no longer exists.
fix: finish v2 removal release gates
…rate-v2

feat(cli): remove the EQL v2 migration leaf
@freshtonic
freshtonic self-requested a review July 29, 2026 06:54

@freshtonic freshtonic left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'm happy to approve. Outstanding feedback has been capture in Linear.

@coderdan
coderdan merged commit d5a2233 into main Jul 29, 2026
18 checks passed
@coderdan
coderdan deleted the remove-v2 branch July 29, 2026 07:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Remove EQL v2 repo-wide (umbrella)

3 participants