Conversation
🦋 Changeset detectedLatest commit: f7b8220 The changes in this PR will be included in the next version bump. This PR includes changesets to release 13 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
…t-dynamodb) PR 1 of the EQL v2 final removal (#707). Delete the closed v2-only dependency chain — @cipherstash/protect-dynamodb → @cipherstash/protect → @cipherstash/schema — and every reference to it. Nothing outside the three imported them (@cipherstash/stack depends only on the separate @cipherstash/protect-ffi). They are superseded by @cipherstash/stack: - @cipherstash/protect -> @cipherstash/stack - @cipherstash/schema -> @cipherstash/stack/schema - @cipherstash/protect-dynamodb -> @cipherstash/stack/dynamodb (encryptedDynamoDB) Existing EQL v2 ciphertext stays decryptable through @cipherstash/stack; this removes the v2 authoring/emission surface, not the read path. Reference cleanup (dangling refs that would break build/CI): - e2e/package.json @cipherstash/protect dep edge - root package.json build:js turbo filter - tests.yml protect/protect-dynamodb .env steps (would fail `touch` on gone dirs) and the bun-job test loop - rebuild-docs.yml trigger tag (@cipherstash/protect@* -> @cipherstash/stack@*) - integration-{drizzle,prisma-next,supabase}.yml packages/schema/** path filters - lint-no-hardcoded-runners allowlist entry - e2e package-managers BIN fixture (dead) + two stale source comments Changeset / RC housekeeping: - delete schema-stevec-standard-pin.md (only target was the deleted schema) - prune the three from pre.json initialVersions - add deletion-notice changeset on @cipherstash/stack + @cipherstash/nextjs Meta honesty: SECURITY.md package list, AGENTS.md Repository Layout, nextjs package description.
- Note the intentional @cipherstash/nextjs patch (package.json description edit) - Use PCRE negative lookahead so the stale-reference check excludes protect-ffi
Remove the `eql_v2_encrypted → 2` branch from `classifyEqlDomain`, so the migrate domain-type resolution (`detectColumnEqlVersion`, `listEncryptedColumns`, `resolveEncryptedColumn`) recognises only the self-describing `eql_v3_*` domains. v3 is the sole generation this workspace authors and backfills; a legacy v2 column's version is carried by the manifest's recorded `eqlVersion` (the CLI status renderers already fall back to it), so v2 status output is unchanged. This drops v2 *classification*, not the v2 read path — existing v2 ciphertext stays decryptable via `@cipherstash/stack`. `EqlVersion` keeps its `2` member for manifest-sourced legacy values and the exported function signatures are unchanged. Tests in `version.test.ts` are updated to assert v2 domains are no longer classified (excluded from `listEncryptedColumns`, `null` from the detectors). Decision 6 guard: `classifyEqlDomain` is a source-column classifier for backfill planning and read-only CLI status display — no decrypt/round-trip consumes its `2` result — so dropping v2 here is safe. NOTE: `packages/migrate/src/eql.ts` (the `eql_v2.*` config-lifecycle wrappers) is NOT deleted in this PR. Although it carries no decrypt path, it is hard- imported by the CLI's v2 cut-over/config commands (`encrypt cutover`, `db activate`, `db push`); deleting it would break the CLI build, which is out of this migrate-scoped PR. Its removal must be sequenced with the CLI v2-command removal. PR 2 of the EQL v2 final removal (#707).
…cryption PR 3 of the EQL v2 removal (#707). Makes EQL v3 the sole generation the client authors and writes, while preserving the minimal v2 READ path on the core client and through the DynamoDB adapter. - Add MappedDecryptOperation: the typed client's decryptModel/bulkDecryptModels are now audit-chainable (.audit()/.withLockContext()) instead of a bare Promise, restoring audited decrypt including through encryptedDynamoDB. - Collapse EncryptionV3 into an overloaded Encryption; EncryptionV3 is now a deprecated, type-identical alias. An explicit config.eqlVersion is honored (the migration escape hatch is retained). - BREAKING: remove the DynamoDB v2 WRITE overloads (encryptModel/bulkEncryptModels); decrypt still reads existing v2 items. - Deprecate (retain) ClientConfig.eqlVersion and the ./schema v2 builders for legacy v2 read/migrate; siblings still consume them, so full removal is deferred to a later PR. - Tests: flip the audit-surface type assertions, add runtime audit-forwarding tests (both chaining orders), and v2-read acceptance integration tests (core + DynamoDB). - Update stash-dynamodb and stash-encryption skills and AGENTS.md. Changesets: @cipherstash/stack minor (audit-on-decrypt), @cipherstash/stack major (DynamoDB v2-write removal), stash patch (skills).
Verification of the EncryptionV3->Encryption collapse surfaced a regression: because Encryption now returns the TYPED client for a v3 schema set, stack-supabase's encryptedSupabaseV3 (which casts to the nominal overload and calls decryptModel(row)/bulkDecryptModels(rows) one-arg) hit the typed methods with no table argument and threw on undefined.tableName. The sibling build passed because it is a type/runtime mismatch with no v3 runtime coverage. Fix: the typed client's decryptModel/bulkDecryptModels now tolerate a missing table, degrading to nominal behaviour (decrypt without date reconstruction) instead of dereferencing undefined — restoring exactly what stack-supabase received before the collapse. table stays required for genuinely-typed callers. - Add regression tests: typed client one-arg decryptModel/bulkDecryptModels decrypt without throwing (no reconstruction). - Correct now-stale customer-visible source docs flagged in review: the resolveDecryptResult docblock, the encryptedDynamoDB version-mismatch error string and @example, and the EncryptedDynamoDBConfig JSDoc (all pointed at EncryptionV3 / needless eqlVersion:3). Update construct-guard.test.ts to the new error wording. Follows 686004f.
Re-parameterize EncryptedQueryBuilderImpl over AnyV3Table and inline every EncryptedQueryBuilderV3Impl dialect override, then delete query-builder-v3.ts. Pure refactor: no runtime behaviour or wire encoding change. The decrypt path stays generation-agnostic (decryptModel/bulkDecryptModels), so stored EQL v2 payloads still decrypt (Decision 6).
encryptedSupabase is now the introspecting EQL v3 factory (was encryptedSupabaseV3); encryptedSupabaseV3 kept as a @deprecated type-identical alias. The legacy v2 encryptedSupabase({ encryptionClient, supabaseClient }).from(table, schema) wrapper and EncryptedSupabaseConfig are removed. All *V3 type exports de-suffixed to canonical names with @deprecated *V3 aliases retained. v2 decrypt is unaffected.
Point tests at the folded EncryptedQueryBuilderImpl, drop the removed v2 authoring tests (v2 live suite, v2 wire-encoding block, v2 builder type test), convert the shared execute() error-threading tests to a v3 table, and add canonical-name type assertions.
v2 read via this adapter is intentionally removed; v2 ciphertext still decrypts through the core @cipherstash/stack client. Mixed-generation handling is customer-side (install both), per #707 out-of-scope stance.
…ap 91 -> 71 The EQL v2 removal folded `query-builder-v3.ts` into `query-builder.ts` (2b4e2e9). FTA penalises size superlinearly, so two files that each passed the complexity gate became one that did not: 90.88 + 73.95 -> 105.77, against a cap of 91. That failed the "Analyze v3 complexity" check on #769. `fta-v3.yml` is explicit that the cap is "a ratchet, not an aspiration", so decompose rather than raise it. The 2331-line class becomes a pipeline: column-map.ts ColumnMap - name + capability resolution, the concern every stage needed (was six correlated fields) query-encrypt.ts filter-operand terms: collect, validate, batch-encrypt query-mutation.ts row-data encryption for insert/update/upsert query-dbspace.ts property-space -> DB-space, once query-filters.ts operand substitution onto the PostgREST query query-results.ts decryption + Date reconstruction query-builder.ts the class: recorded state, fluent surface, execute() Worst file 105.77 -> 70.12 (query-encrypt.ts); cap lowered 91 -> 71. Also removed, all no-ops from the pre-fold v2/v3 inheritance: - `notFilterOperator` - an identity function with an unused parameter - `applyPatternFilter`'s dead `_wasEncrypted` parameter - `protected` throughout, now `private`; nothing extends the class and collapsed the six-times-repeated withLockContext/audit/await dance into one `withOpContext` helper - a skipped lock context would encrypt under the wrong data key, so the three steps must stay identical. `EncryptedQueryBuilderImpl` keeps its name, export site and constructor; nothing here is part of the package's public surface (index.ts does not re-export it). 466 tests pass unchanged. One test reached the unsupported- queryType backstop by subclassing to override `queryTypeForRawOp`, which is now a module function; `assertTermQueryable` is exported instead and the test calls it directly, dropping a subclass its own comment called "breaking the internal contract". Docs: correct comments asserting adapter-side EQL v2 reads. The adapter is v3 only and does not auto-read `eql_v2_encrypted` columns - introspection matches `public.eql_v3_*` domains exclusively, so such a column never enters the encrypt config. No ciphertext is stranded: core decryption stays generation-agnostic. Also retires the two-dialect scaffolding in types.ts, whose header claimed v3 does free-text via `contains` - contradicting the typed surface forty lines above it and re-introducing the exact confusion #617 removed. `EncryptedQueryBuilderCore`'s OK/BK defaults are kept: they are live for the untyped surface, only their v2-era rationale was wrong.
Delete the EQL v2 authoring surface (`src/index.ts` `encryptedType` + config extraction, `src/operators.ts`, `src/schema-extraction.ts`) and promote the EQL v3 implementation from `./v3` to the package root as a hard break with no alias. - Drop the `./v3` subpath from the `exports` map and remove `typesVersions`; the v3 impl now lives at `src/*` and is the sole `.` export. - De-suffix the public API: `createEncryptionOperatorsV3` -> `createEncryptionOperators`, `extractEncryptionSchemaV3` -> `extractEncryptionSchema`. No `*V3` aliases (they would type-check v2 call sites against v3 semantics). - Delete the two v2 operator test files; move the v3 test suite to `__tests__/*` and repoint imports. ESM+CJS both preserved. - Fix the `@cipherstash/bench` importer: rewrite the Drizzle table to v3 `types.*` domains + `EncryptionV3`, switch operator usages off the removed v2-only ops (`like`/`ilike`, jsonb-path) to `matches`/`contains`/`selector`, and update the bench fixture schema.sql to the eql_v3 domains/index terms. - Update the README and the bundled `stash-drizzle` skill to the collapsed root imports; remove v2 authoring guidance. Existing v2 ciphertext still decrypts via `@cipherstash/stack`; only the Drizzle-side v2 authoring/query-building is removed (Decision 6). BREAKING CHANGE: `@cipherstash/stack-drizzle` no longer exports an EQL v2 surface and the `./v3` subpath is removed. Import the v3 API from the package root with the de-suffixed names.
…ollapse PR 3 made EncryptionV3 an overloaded alias; ReturnType<typeof EncryptionV3> now resolves to the nominal overload (EncryptionClient) not the typed client. Infer through a single-signature helper so the bench handle keeps the typed client type.
…the image The bench fixture schema moved to concrete EQL v3 domains (`public.eql_v3_text_search`, `eql_v3.eq_term(...)`), but tests-bench.yml started `postgres-eql:17-2.3.1` — which ships EQL v2 — and ran the suite immediately. `applySchema` failed with `type "public.eql_v3_text_search" does not exist`. Nothing tied the image tag to the EQL version the fixtures need, so the coupling broke silently. Install v3 the way every integration suite already does: a vitest `globalSetup` calling test-kit's `installEqlV3`, which shells out to the real `stash eql install --eql-version 3`. That needs only a database URL — no CipherStash credentials — so `db-only.test.ts` stays the credential-free smoke test it is meant to be, and CI and `pnpm test:local` become the same path rather than CI depending on a step the local flow never ran. The globalSetup imports `@cipherstash/test-kit/install` (a new narrow subpath export) rather than the barrel: the barrel reaches `needle-for.ts`, which consumes stack source through the `@/` alias, and bench has no `stackSourceAlias` in its vitest config. `install.ts` depends on node builtins only. Workflow changes: - Use the `integration-setup` composite, which already builds the `stash` CLI the install needs (pinned to this job's existing Node 22). - Start only the `postgres` service; docker-compose.yml also defines a PostgREST that bench never talks to. - Widen the trigger paths to follow the package graph. bench depends on `@cipherstash/stack` and `@cipherstash/stack-drizzle`, and now on the CLI's EQL installer, but the filter covered only `packages/bench/**` and `local/**` — a break upstream could never trigger this job. No changeset: `@cipherstash/bench` and `@cipherstash/test-kit` are both private, and workflow files are repo tooling.
… scaffold, stale shipped docs Six review findings, each with the regression test that would have caught it. - `packages/bench/sql/schema.sql` indexed `eql_v3.ste_vec(enc_jsonb)`, a mechanical rename of the v2 expression. It builds, and `db-only.test.ts`'s `pg_indexes` check passes, but nothing the adapter emits ever mentions it: `@>` on `eql_v3_json_search` inlines to `eql_v3.to_ste_vec_query(a)::jsonb` (the bundle's own comment says so). A permanently-dead index in the one fixture whose job is to prove index engagement. Now indexed on the inlined expression, with `scripts/__tests__/bench-index-expressions.test.mjs` pinning each bench index against the operator body in the vendored EQL bundle — no database, no credentials, which matters because the bench's own EXPLAIN assertions need credentials and never run in CI. - `stash init --drizzle` scaffolded `extractEncryptionSchemaV3` from `@cipherstash/stack-drizzle/v3` into the user's repo — both removed here, so a freshly-initialised project would not resolve. Flipped the drizzle half of the generated strings (the `@cipherstash/stack/v3` half stays for PR 7) and pinned it in `utils-codegen-drizzle.test.ts`. - `skills/stash-encryption`, `packages/stack/README.md` and `AGENTS.md` still documented the `./v3` subpath and the `*V3` names. Both skills and the stack README are shipped artifacts. `no-removed-drizzle-surface.test.mjs` scans the shipped file set (deliberately not CHANGELOGs or specs, which should still name the old surface). - `vitest.shared.ts` aliased `@cipherstash/stack-drizzle/v3` to the deleted `src/v3/index.ts`; `vitest-shared-alias.test.mjs` asserts every alias target exists on disk. - The bench `matches` operand was the shared `value` prefix, which every seeded row contains — the bloom index had nothing to narrow, so the number measured a full scan. Uses a full seeded value now. - The bench importer's only CI gate was the Bun job's `turbo build`, which swallows its own test failures. Added an explicit typecheck step to the main test job. The eq/matches index assertions are sound and now say why: the operator wrappers are `LANGUAGE sql IMMUTABLE STRICT` single-SELECT bodies, so the planner inlines them and applies the same inlining to the stored index expression. The old comment claimed the adapter emitted `eql_v3.eq_term(col) = eql_v3.hmac_256(value)` — it emits `eql_v3.eq(col, term)`, and `eql_v3.hmac_256` does not exist in the bundle at all.
Addresses PR #760 review feedback. - `docs/query-api-walkthrough.md` pointed at `packages/protect/src/ffi/*`, deleted by this PR. Replaced with a note rooting the doc's relative paths, and corrected the two other stale facts in the same block: the protect-ffi pin (0.24.0/0.23.0 → the actual 0.30.0) and row 1a's query-builder paths, which moved to stack-drizzle/stack-supabase in the #627 split. - `.github/dependabot.yml`'s ignore-rule comment narrated the #673 incident in the present tense against a package that no longer exists. Kept the lesson, marked the package as removed, and stated why the rule still holds for the surviving consumers. - `.github/workflows/tests.yml` looped over `packages/stack-forge`, which has never existed. The `[ -f ]` guard made it a no-op; dropped it. `scripts/lint-no-dead-package-paths.mjs` fails CI on any `packages/<name>` reference that doesn't resolve to a directory, across docs, .github, skills and the root meta files. Design archives (docs/plans, docs/superpowers) and CHANGELOGs are exempt — they record history, not the current tree. It catches all three of the above; self-tests follow the existing scripts/__tests__ pattern.
…change `classifyEqlDomain` now returns `3 | null`, so `listEncryptedColumns` can never emit `version: 2` and the `c.version === 2` exemption in `explainUnresolved` is dead. Removing it is provably behaviour-preserving: a post-cutover v2 table (`<col>` carrying the v2 domain) now reaches `explainUnresolved` with an EMPTY candidate list, which the `candidates.length === 0` guard above already falls through on. - Remove the branch; rewrite the doc comment and the stale v2 parentheticals in `drop.ts` / `cutover.ts` to say why the post-cutover state now arrives as "no EQL columns". - Rewrite the drop test that hand-built a `version: 2` candidate — a state resolution can no longer produce, so it only exercised the dead branch — to the state that actually occurs. - Add unit tests pinning `explainUnresolved`'s contract, including that a candidate sharing the plaintext column's name still fails closed (the removed branch's only behaviour, and wrong at v3, which has no cut-over rename). - Correct the backfill manifest comments: `null` now also means a legacy `eql_v2_encrypted` domain, so a v2 column backfilled from here on records no `eqlVersion` and reports no version in `encrypt status`. The live-domain fallback yields null for that case too. Existing manifests are unaffected. Noted in the changeset. - Drop the PR-1 plan doc that landed in this PR's diff.
- MappedDecryptOperation.execute returns a fresh Result on the unknown-table path, so ops can't alias/mutate a shared failure object. - Document the AnyV3Table cast in Encryption with a biome-ignore explaining the runtime isV3Only guard the compiler can't see. - Restore CS_WORKSPACE_CRN in decrypt-audit-forwarding.test.ts afterEach so it doesn't leak env state across suites. Verified the v2-read acceptance tests (integration/shared/v2-decrypt-compat) match the CI CS_IT_SUITE glob, so #1a/#1b run under test:integration.
…ent lock-context re-bind
- resolveDecryptResult: the inner comment and `logger.debug` message still
said the typed client has no decrypt audit surface and told the reader to
use `Encryption({ config: { eqlVersion: 3 } })`. Both shipped clients now
carry `.audit()` on decrypt, so the branch only fires for a non-conforming
custom client. Message rewritten to describe that, with a regression test
asserting it names neither `eqlVersion` nor `EncryptionV3`. The same false
claim had propagated to the resolve-decrypt test header, `dynamodb/types.ts`,
a test literally named "though decrypt cannot carry it", and a comment in
`stack-supabase` — all corrected.
- MappedDecryptOperation.withLockContext: chaining a second lock context onto
an already-bound op silently dropped it. The wrapper always exposes the
method, unlike the nominal path where it is absent after binding, so the
re-bind type-checks. It now throws. Verified no internal or sibling call
site chains after a positional bind (stack-supabase calls `decryptModel(row)`
one-arg then chains, so its underlying op is unbound). Tests cover both
`decryptModel` and `bulkDecryptModels`.
- Changeset bumped minor -> major. Making `Encryption({ schemas: [<v3>] })`
return the typed client changes its return type AND adds `Date`
reconstruction on the two-arg `decryptModel` for existing plain-`Encryption`
v3 callers. The package already carried a major, so the released bump is
unchanged — the changelog is just accurate now.
- skills/stash-encryption documented `decryptModel`/`bulkDecryptModels` as
returning `Promise<Result<…>>` and said only encrypt-side ops are chainable.
It ships in the `stash` tarball, so that was wrong guidance in customer
repos. Updated, including the one-or-the-other lock-context rule.
…array CodeRabbit review of the query-builder split. Six findings, all pre-dating the refactor; these are the two worth acting on. `single()`/`maybeSingle()` have always returned ONE object at runtime, but returned `Self`, so the builder kept advertising the array shape it was created with — `data` was typed `T[] | null` while holding a single row. Callers had to launder it, and the test suite documented the lie in a comment while casting through `unknown` to reach the row's fields. Both now return `EncryptedSingleQueryBuilder<T>`, awaiting `EncryptedSupabaseResponse<T>` (`data: T | null`) — which already covers the zero-row case for `maybeSingle()` and the error case for both, so no separate null modelling was needed. The impl class carries the awaited shape as a `TData` parameter so the promise cannot keep advertising `T[]` after the runtime has been switched to single-row mode; `returns<U>()` preserves that shape. Filters and transforms are deliberately absent from the single-row builder, matching supabase-js: applying one after `single()` would change the query the single-row promise was made about. Also drops two unnecessary `as unknown as T[]` bridges in query-results.ts (`[] as T[]` and the bulk-decrypt map both compile directly). Not acted on, with reasons: - The missing `assertPostgrestCanQueryEncryptedOperator` in the not-filter branch is a false positive. The guard fires upstream in `assertTermQueryable` (`contains`/`matches` map to `freeTextSearch`), before encryption, and `supabase-v3-json.test.ts` already covers `.not(col,'contains')` and `.not(col,'matches')` under EQL 3.0.2. The suggested guard keys on `wasEncrypted`, which that path never reaches. - Routing plaintext `in` arrays through `formatInListOperand` would change behaviour: `.filter()` is the raw escape hatch and forwards verbatim, as supabase-js does. The encrypted path only intervenes because it must encrypt element-wise. - The `as never` on `bulkEncrypt` args and the `term.column` double assertion need `ScalarQueryTerm['column']` widened in `@cipherstash/stack` — a different package's public type.
CI runs against the PR MERGE commit, so it saw a file this branch never had: `remove-v2` gained `utils-codegen.test.ts` (from the v3 domain-picker work) after this branch was cut, and it pins the generated `stash init` client to `extractEncryptionSchemaV3` / `@cipherstash/stack-drizzle/v3` — exactly the two names this PR removes. Textually the merge is clean; the conflict is semantic, which is why it only surfaced in CI. Points those assertions at the collapsed root and adds the matching negatives, so a regression to the `./v3` specifier fails here rather than in a scaffolded project. Narrowed `utils-codegen-drizzle.test.ts` to what the merged-in suite does not cover — `generatePlaceholderClient`, still untested — and updated it to the new `ColumnDef` shape (`domain`, not `dataType`/`searchOps`).
The wizard now scaffolds EQL v3 columns, so `drizzle-kit generate` emits `ALTER COLUMN ... SET DATA TYPE eql_v3_<name>` — which Postgres rejects (no cast from text/numeric to an EQL domain). The post-agent rewriter matched only the single `eql_v2_encrypted` type, so those v3 statements slipped through unrepaired and failed at migrate time. Port the rewriter to the whole `eql_v3_*` concrete-domain family alongside legacy `eql_v2_encrypted`, mirroring the sibling CLI fix (#693): every mangled form drizzle-kit emits (incl. the 0.31.0+ `"undefined".` prefix and schema-qualified pgSchema tables), near-miss flagging for `SET DATA TYPE ... USING ...` it cannot safely repair, statement-breakpoints, and a clearer data-destroying / empty-table-only warning that points populated tables at the staged `stash encrypt` flow. Database introspection (`isEqlEncrypted`) now recognises BOTH `eql_v2_encrypted` and the `eql_v3_*` family as already-encrypted, matching migrate's `classifyEqlDomain` v3 convention — so the agent won't scaffold over existing encrypted data of either generation (v2 ciphertext stays valid and detected). Add a rewrite-migrations test suite (adapted from the CLI's).
Address code-review feedback on the v3 migration-rewriter port.
**Sweep every candidate migration directory.** `rewriteEncryptedMigrations`
returned after the FIRST candidate that merely existed, even when that
directory contained zero matches — so an empty or already-rewritten
`drizzle/` sitting next to a project's real `migrations/` left those
migrations unrepaired, and they then failed at migrate time with the very
`cannot cast type ...` error the rewriter exists to prevent.
The comment justifying the early return ("running again on a different
candidate would double-transform already-rewritten SQL") was wrong on both
counts: distinct directories hold distinct files, and the rewrite is
idempotent anyway — a rewritten statement no longer contains `SET DATA
TYPE`, so neither the strict matcher nor the near-miss scan can match it a
second time.
Extract the sweep into an exported, log-free `sweepMigrationDirs(cwd, dirs)`
so it is directly testable without mocking @clack/prompts. It sweeps every
existing candidate and reports a directory that throws via `error` rather
than stranding the ones after it; post-agent keeps the reporting.
**Trim the near-miss statement preamble.** `NEAR_MISS_RE` opens with a lazy
`[^;]*?` whose only left boundary is the previous `;` — or the start of file
when there is none. The reported statement therefore dragged in every
comment and blank line since then, so a near-miss in a file opening with a
comment block was quoted back to the user with the whole header glued to its
front. Strip leading blank/comment lines (incl. `--> statement-breakpoint`)
so the statement reads as the offending statement alone. Detection is
unchanged; only the text shown to the user differs.
Applied to the `stash` CLI sibling too — it carried the identical defect and
its header mandates keeping the two in sync. The directory sweep does not
apply there: the CLI takes a single explicit `outDir`.
**Document the hardcoded `"public"."<domain>"`.** Behaviour unchanged and
not a regression, but worth stating: the domain qualifier is an assumption
(EQL installs into `public`), not something read back from the matched SQL —
the `schema` capture is the TABLE's schema and says nothing about where the
domain lives. Non-public domain installs would need it threaded in here and
in the CLI sibling. Already pinned by the existing pgSchema() test.
9 new tests across the two packages, each watched failing first.
…ypecheck Closes the last open item from the #771 review — the 3 pre-existing `AutoStrategy` tsc errors flagged as "would bite a future strict type gate". **Root cause.** `@cipherstash/auth` uses conditional exports: `node` resolves to `index.d.ts` (the full surface, including `AutoStrategy`), `default` resolves to `wasm-types.d.ts` (which has no `AutoStrategy`). The wizard's tsconfig sets `moduleResolution: "bundler"`, which does NOT include the `node` condition, so tsc took the `default` branch and reported `AutoStrategy` as missing on all three call sites — `agent/fetch-prompt.ts`, `agent/interface.ts` and `lib/prerequisites.ts`. Nothing was actually broken at runtime: the wizard is a Node CLI and always loads the `node` branch. The types were simply resolved against the wrong entry point. **Fix.** Add `"customConditions": ["node"]`, matching what `packages/stack`, `packages/prisma-next`, `packages/test-kit`, `packages/stack-drizzle` and `packages/stack-supabase` already carry for the identical `protect-ffi` conditional-export problem. `tsc --noEmit` now exits 0. **Regression guard.** A type-level defect needs a type-level gate, or it comes back silently — the wizard is built by tsup with `dts: false`, so the build transpiles without ever typechecking, which is exactly why these three errors sat in `main` unnoticed. Add a `typecheck` script and wire it into `tests.yml` alongside the prisma-next gate (#684). Verified the gate fires: with `customConditions` removed the step exits 2 on all three errors; restored, it exits 0. No changeset — `dts: false` means the published tarball is byte-identical. Tooling only, no observable behaviour change. Green: wizard 265 pass / 5 env-skipped, typecheck 0 errors; stash 770 pass / 53 files; `code:check` 0 errors.
Rebase fallout: main's `stash-indexing` skill (#773) and the indexing section it added to `stash-drizzle` both import `encryptedIndexes` from `@cipherstash/stack-drizzle/v3`, a subpath this branch removes when it collapses `./v3` into the package root. Both files ship to customer repos, so the stale specifier would not resolve in a freshly-initialised project. Caught by `scripts/__tests__/no-removed-drizzle-surface.test.mjs`, the guard this branch added for exactly this drift.
…sic to v3 PRs 3-5 of the EQL v2 removal froze the target names, but nothing type-checks the artefacts that describe them, so two were left wrong. `stash init` scaffolded `EncryptionV3` into customer source. That name is a deprecated alias now, and the scaffold is a template literal, so only an assertion can catch it — the codegen tests pinned the old form, so they are flipped with negatives added, mirroring the drizzle precedent from 7b783ee. `@cipherstash/stack/v3` exported `EncryptionV3` but not `Encryption`, so the scaffold could not emit a single clean import. Re-export it — the deprecation example in v3.ts already told users to import it from there. `examples/basic` had not compiled since the removal deleted `encryptedType` and the v2 `encryptedSupabase`. Ported to the v3 types.* factories. Its Supabase branch is deleted rather than ported: it imported a `contactsTable` that was never exported, so it was already dead before this work, and examples/supabase-worker carries the Supabase story. Root `build`/`test` filter to ./packages/*, so CI never compiled any example and the breakage sat on a green board. Gate examples/basic through a new turbo `typecheck` task so `^build` builds its deps first. Verified the gate fails on the exact regression that shipped.
The v3 overload accepted `config: { eqlVersion: 2 }` and an empty schema tuple,
neither of which yields the typed client it promised — the first returns the
nominal client at runtime (silently discarding decryptModel's table and lock
context), the second throws. It now takes a non-empty tuple and V3ClientConfig.
`EncryptionClientFor<S>` is exported because `ReturnType` reads the last
overload and so always names the nominal client.
decryptModel/bulkDecryptModels gain the table-less overload the runtime already
supports — the path for reading legacy v2 models, whose table cannot belong to a
v3 schema tuple. v2-decrypt-compat now covers a v3 client reading v2 ciphertext
rather than only round-tripping v2 through the emission path being removed.
…ants `EncryptedSingleQueryBuilder` was declared but missing from the type-export allowlist, and its changeset documented a `.single().returns<U>()` that did not compile. Removes the unreachable v2 leftovers: `getEncryptedColumnNames`, the v2 `addJsonbCasts`, `returnType: 'composite-literal'` on v3 terms, and four unused type imports. Corrects the inverted postgrest-js parity comment.
The doctrine written into customer repos still called the extension `eql_v2.*`, setup-prompt still named the removed `encryptedType` and the v2 `encryptedColumn` builder, and four skills claimed the rollout tooling auto-detects a column's version — detection is one-sided, so a v2 column classifies as unknown and records no version. Also fixes the `--drizzle`/`--migration` flags that now require `--eql-version 2`, the stale `# v2 (default)` in the Supabase reference, and the README section routing users onto the deprecated v2 builders.
…claims tests-bench.yml watched packages/stack/src/eql/v3/** but the bench client comes from src/encryption/**, so bench-relevant changes never triggered it. vitest.shared.ts gains the missing @cipherstash/test-kit/install alias. The drizzle changeset advertised encryptedIndexes on the deleted /v3 entry, and the DynamoDB one claimed a runtime guarantee the type narrowing does not make.
`rewriteEncryptedMigrations` folded a failed directory sweep away: the `error` branch logged a warning and `continue`d, contributing 0 to both totals, so `destructive = rewritten > 0 || skipped > 0` stayed false and `p.confirm` was handed `initialValue: true`. A directory the sweep could not read looked exactly like a clean one, and the case where hitting Enter is least safe got the friendliest prompt. `packages/cli` already settled this the other way — `eql/migration.ts` and `db/install.ts` track a `sweepIncomplete` flag that deliberately treats "failed outright" and "left near-misses" as one state. Match it. The sweep now reports `failedDirs`, and the caller separates `unsafe` (rewritten or flagged) from `unverified` (unswept). Either defaults the prompt to No, but only `unsafe` claims the migration destroys data — nothing is known about an unswept directory, so the prompt names it and says it went unchecked instead. The failure test is `error !== undefined`, not truthiness: `error` is `err.message`, and `new Error()` has an empty one, which would otherwise fall straight back onto the fail-open path.
`from<Row>()`, `returns<U>()` and `single().returns<U>()` constrained the row parameter to `Record<string, unknown>`. An `interface` carries no implicit index signature, so the most ordinary way to declare a row type failed with TS2344 while an equivalent `type` alias compiled. Upstream postgrest-js constrains `returns` to nothing at all, so the adapter was stricter than the API it mirrors. Relaxed to `object` — which still rejects scalar row types — across the whole chain, since `EncryptedQueryBuilderUntyped` and `EncryptedQueryBuilderCore` re-impose the constraint on the array builder's return type. `decryptResults` moves with them; its row parameter only supplies the default for `TData`, which every call site passes explicitly. The existing type tests missed this because they declare the row as `type UserRow = InferPlaintext<typeof users>`. The new case uses a real `interface` and carries a comment saying why it must stay one. Also corrects the `EncryptedSingleQueryBuilder` docs, which claimed everything re-typing or re-configuring the request is carried over past `single()`. `overrideTypes` and `setHeader` are not — and since `single()`/`maybeSingle()` return the same instance rather than a passthrough, calling them fails at runtime, not just in the checker.
The three rollout skills described the EQL v2 lifecycle as the unqualified default even though v3 is the default generation. Worst case, `stash encrypt drop` was documented as removing `<col>_plaintext` — that is the v2 target. On a v3 column there is no `<col>_plaintext`: `drop.ts:113-118` drops the ORIGINAL `<col>`, inside a `DO` block that takes ACCESS EXCLUSIVE, re-counts unencrypted rows at apply time and raises rather than dropping if any remain. Also corrected: - "the pending row will be promoted by `stash encrypt cutover`" is false for v3 — cutover short-circuits before touching any config, so `stash db activate` is the only promotion path. - The Proxy call-outs told every reader to run `stash db push`. It manages `eql_v2_configuration`, which v3 does not ship: on a v3-only database it reports "Nothing to do." and exits 0. - The CLI sequence blocks were unlabelled v2 recipes contradicting the table above them. v3 now comes first, since it is the default, and the v2 block is scoped rather than deleted. - drizzle-kit emits the BARE `eql_v3_text_search`; the schema is stripped deliberately, so the documented DDL was invalid. Adds the unit coverage `resolveEqlVersion` has always claimed to have: its `@internal` marker says "exported for unit-test coverage of the detection matrix" and there was none, so the only exercise of the v3 wire choice needed live credentials. Pairs with a guard in the v2-decrypt compat suite pinning that client to v3 wire — without it the whole `#1c` block passes vacuously if detection regresses, because a v2-mode client reads v2 payloads natively.
`classifyEqlDomain` returns `3` for an `eql_v3_*` domain and `null` for everything else (`packages/migrate/src/version.ts:45-50`). Detection is one-sided: a v2 column is never detected as v2 — it classifies as unknown and reaches the v2 lifecycle by fallback. It also records no `eqlVersion` in the manifest, since `backfill.ts:572` only writes the field when it is truthy. Three shipped surfaces said otherwise: - `skills/stash-supabase/SKILL.md` claimed auto-detection of a v2 column in three places, one of them inside the "Stay on v2 for now" bullet — precisely the case it got wrong. The same file already carried the correct one-sided wording in its EQL version note, so the three now match it. This skill is copied into customer repos by `stash init`. - `packages/migrate/README.md` documented `detectColumnEqlVersion` as returning `2`, `3`, or `null`. It cannot return `2`. That is the hardest of the three to excuse — it states an exported function's return type. - `packages/stack/README.md`'s headline Supabase example imported and called `encryptedSupabaseV3`, the `@deprecated` alias, contradicting the package table and the v3-only note further down the same file. Documentation only — no behaviour change.
Three changeset defects, all of which render into the 1.0.0 CHANGELOG.
`eql-v3-sole-docs.md` named the taught surface as `EncryptionV3` /
`@cipherstash/stack-drizzle/v3` / `encryptedSupabaseV3` — the first two
of which the docs no longer teach and the third of which is a
`@deprecated` alias — and advertised a `/v3` subpath this release
deletes. Its two "exceptions" were also stale in present tense:
DynamoDB does not "still require the v2 schema surface" (writes are
v3-only per `dynamodb/types.ts:240`; only the decrypt overloads keep a
v2 table), which contradicted `stack-dynamodb-v2-write-removal.md` in
the same release, and the rollout tooling does not "currently target v2
columns" — v3 is the recognised and default generation.
`init-placeholder-eql-v3.md` said init scaffolds `EncryptionV3` and
`extractEncryptionSchemaV3` from `/v3`; it emits `Encryption` and the
collapsed root (`init/utils.ts:288-289`), contradicting
`remove-eql-v2-drizzle-root.md:25`. That text is already published in
`packages/cli/CHANGELOG.md`, and it was accurate for rc.0 — so it gets a
"superseded later in this release" pointer rather than a rewrite. The
same claims live in `init-drizzle-eql-v3.md`; editing one and not the
other would have created a fresh contradiction.
Adds the missing `@cipherstash/prisma-next` changeset for the
`encryptionConfig` narrowing from `ClientConfig` to `V3ClientConfig`
(`from-stack-v3.ts:65`), which makes `{ eqlVersion: 2 }` a TS2322. Ten
changesets name that package and none mentioned it.
Deliberately untouched: thirteen `/v3`-subpath and `*V3` references
across seven other changesets. Those describe surfaces that genuinely
shipped in rc.0-rc.4 and are already published; rewriting them would
make the collated 1.0.0 CHANGELOG diverge from what users received.
Two ways the comment guard could still be walked past into a live
`DROP COLUMN`. Both reproduce in the wizard and the CLI copy.
An apostrophe inside a double-quoted identifier opened a phantom string
literal. `sql.indexOf("'", i + 1)` then ran to the NEXT apostrophe —
typically the same identifier in a commented-out ALTER further down —
concluded that ALTER was live, and rewrote it. A column named
`"o'brien_data"` was enough, and the CREATE that declared it sits above
the ALTER in any real corpus, so the ordering the bug needs is the
ordering migrations naturally have.
An `ALTER` quoted inside an `INSERT … VALUES ('…')` is data, but was
rewritten as SQL. That splices `--> statement-breakpoint` markers inside
the literal, so splitting the file the way drizzle's migrator does
yields a bare, live `ALTER TABLE … DROP COLUMN …;` as a chunk of its
own. It is non-destructive today only because the preceding chunk hits a
syntax error first and drizzle aborts the transaction — a property of
the runner, not of what we emitted. Escaping the injected text's own
apostrophe would have fixed the syntax error and left the DROP COLUMN.
`isInsideComment` becomes `isInsideCommentOrString`: double-quoted
identifiers are consumed before `'` is considered, an unterminated or
index-spanning literal counts as inert, and a doubled `''`/`""` is an
escape. Dollar-quoted bodies stay untracked, which is now strictly
fail-safe — an apostrophe in one can only cause a skip, never a
destructive rewrite.
The two implementations remain character-identical: diffing them
comment-stripped yields only the three sanctioned deltas (the
`existsSync`/`resolve` imports, the wizard-only `sweepMigrationDirs`
block, and the attribution string). Neither new function appears in that
diff.
Also pins two cases that already behaved correctly but were untested:
lowercase `alter table … set data type`, and a commented-out ALTER with
CRLF line endings.
Self-review of #780 found the previous commit reintroduced its own bug one branch over. The `'` branch treats an unterminated literal as inert and returns early; the `"` branch just advanced the cursor. On an unterminated identifier that set `i = sql.length`, so the `while (i < index)` loop exited and the function returned `false` — "live" — and every commented-out ALTER below it was rewritten into a real DROP COLUMN. Exactly the failure the apostrophe fix was for. Reachability is low: an unpaired `"` outside a comment or literal is malformed SQL, and drizzle does not generate one. But the sweep runs over whatever migration directories a project has, the failure is silent, and the cost is a dropped populated column — the same reasons the corpus heuristic elsewhere in this file is written to fail closed. Both branches now return early on an unterminated quote. An ALTER match offset never lands inside an identifier, so nothing changes for well-formed input.
…crypt The `#1c` integration case added by 7e0092f went red in CI, and it was right to. `encryptedDynamoDB` documents that writes are EQL v3 only but decrypt keeps accepting a v2 table so previously stored items stay readable. Both decrypt operations then forwarded that table to the encryption client unconditionally. The second argument is only meaningful to the typed v3 client, which resolves it against its own reconstructor map. A legacy v2 table is not in that map, so a v3-configured client reading a stored v2 item failed with "decryptModel received a table this client was not initialized with" — precisely the customer the compatibility promise is written for. The comment above the call claimed the argument was "ignored by the nominal one", which was true when only a nominal client existed and stopped being true when the typed client arrived. Forward the table only when `isV3Table`; otherwise use the table-less form and let the client derive the table from the payloads. `bulkDecryptModels` carried the identical unconditional forward with no test on it at all, so it is fixed in the same commit. Both paths now have credential-free coverage asserting what the adapter forwards. The only previous coverage was a live-ZeroKMS integration suite, which is why this reached released code: it is present on `main` as well as this branch.
The note explained why `returnType` is absent, then justified it with what the field used to do on the `encryptQuery` path that the same PR deleted. `returnType` appears nowhere else in this package — it is not a field on anything this function builds — so the comment described the absence of something a reader cannot find, and pointed at code they cannot read. From ae5d1a4, "drop dead v2 remnants".
Same shape as the `query-encrypt.ts` note removed in 4c17678: each justified current behaviour by narrating a code path this stack deleted, which a reader cannot look up. - `resolve-eql.ts` — a parenthetical about a `version: 2` candidate that "used to arrive here and needed its own exemption". The sentence before it already says the column never appears as a candidate, which is the whole point. - `dynamodb/types.ts` — an em-dash clause attributing the wrong typing to "the removed v2 write overload". The sentence is unchanged without it, and this JSDoc ships in the published `.d.ts`. - `install-eql.ts` — eight lines narrating an `eqlVersion: '2'` pin that no longer exists. Rewritten to state the constraint that still holds: `eql install --drizzle` is v2-only, so routing Drizzle through it would provision a v2 database and contradict the v3 skill installed into the same project. Comment-only; no non-comment line changed. Kept the past-tense notes that describe fixed BUGS rather than removed APIs — a reader can still check those against the code that would reintroduce them.
…tion fix: review remediation for #772 — rewriter data loss, supabase row types, shipped-doc corrections
…lares A column missing from the encrypted index was treated as plaintext and rewritten into ADD+DROP+RENAME. Absence is not evidence: the declaration can live in a migration directory the sweep never reads, so the DROP COLUMN could land on a column already holding ciphertext. The corpus index now records declared columns as well as encrypted ones, and an ALTER whose column is declared nowhere is reported as source-unknown instead of rewritten.
…in the rewrite sweep
Code review of the fail-closed rewrite (3fedb33e) found two Important
defects, both traced to the plan's own supplied code rather than its
transcription:
- DECLARED_COLUMN_RE matched any quoted name followed by whitespace and
a letter, so a mention inside a predicate or constraint — e.g.
CHECK ("email" IS NOT NULL), or a constraint named the same as a
column — registered as a declaration with no real declaration ever
existing, walking the fail-closed sweep back onto the fail-open path
it exists to close. Fixed with a word-boundary-pinned negative
lookahead over the SQL keywords reachable in that position, verified
against every real type token that shares a keyword's leading
letters (interval/inet/int vs IN, char/citext vs CHECK,
eql_v2_encrypted/eql_v3_* vs EXCLUDE, etc).
- The per-column comment check landed on BOTH body scans inside
indexColumnDeclarations, but over-detecting "encrypted" (safe) and
over-detecting "declared" (not safe) are not symmetric. Applying it
to the encrypted scan turned a commented-out encrypted column inside
an otherwise-live CREATE TABLE from over-detected (safe, pre-existing
behaviour) to under-detected — exactly the "irrecoverable ciphertext"
case the function's own docstring warns against. Removed the check
from the encrypted scan only; kept it on the declared scan.
Also corrected two doc comments that asserted the old (wrong) behaviour,
and three minor JSDoc/comment inaccuracies (skip-reason count, missing
source-unknown in the exported contract, a comment naming the wrong set).
Added tests pinning both fixes: a CHECK predicate mention and a
same-named constraint no longer count as declarations; a type name that
merely starts with a blocked keyword's letters (interval/inet) still
does; and a commented-out encrypted column beside a live redeclaration
still reports already-encrypted rather than being rewritten.
…declares Mirrors the CLI fail-closed fix (3fedb33e, bcc573c6) into the wizard's copy of the sweep. This is where the fail-open default bit hardest: the wizard ships scanning drizzle/, migrations/ and src/db/migrations/ with a per-directory index, so a column declared in one directory and altered in another was rewritten on an assumption, and the ADD+DROP+ RENAME dropped live ciphertext with nothing anywhere to backfill from. Also documents two residual gaps found in Task 1's review, in both copies: a REFERENCES ... ON DELETE CASCADE mention can coincide with a column name (DECLARED_COLUMN_RE's keyword lookahead doesn't cover ON), and ADD_COLUMN_RE needs no such lookahead since the token after an ADD COLUMN's column name is always its type. Ports the CLI's full current test coverage (not just the original 7 tests, since a second review-fix commit added 4 more pinning the keyword lookahead and the comment-check asymmetry) plus a wizard-specific regression test for the per-directory index gap.
…gration tests The Drizzle migration-sweep test fixtures wrote a bare ALTER COLUMN ... SET DATA TYPE with nothing declaring the target column anywhere in the corpus. Since rewriteEncryptedAlterColumns is now fail-closed, such a statement is correctly left as a source-unknown skip, which broke two assertions that expected the rewrite to happen. These tests exist to check the sweep's behaviour (that it walks sibling migrations and skips the just-generated install migration), not the fail-closed rule itself, so fix the fixtures rather than the assertions: each now writes a sibling CREATE TABLE declaring the column plaintext, matching the declarePlaintext pattern already used in rewrite-migrations.test.ts.
… post-agent test The 'defaults to No, and says why, when a file was rewritten' fixture wrote a bare ALTER COLUMN ... SET DATA TYPE with nothing declaring the target column anywhere in the swept corpus. Since rewriteEncryptedAlterColumns is fail-closed, that statement was correctly left as a source-unknown skip — the test passed anyway only because post-agent.ts's `unsafe` check is `rewritten > 0 || skipped > 0`, so it silently became a duplicate of the skipped-path test below it, and the `rewritten > 0` half of that condition had zero coverage. Add a sibling migration declaring the column plaintext (matching the declarePlaintext pattern already used in rewrite-migrations.test.ts and the analogous cli/eql/migration.test.ts fix), so the sweep genuinely rewrites the column and the test exercises the branch its name claims.
Both skills said every recognised statement is rewritten. It now requires the column's declaration to be present in the swept directory and flags what it cannot place.
…the migration corpus can see The changeset and skills/stash-cli/SKILL.md both claimed the fail-closed sweep means the rewrite "can no longer drop a column that already holds ciphertext." That overclaims: the sweep requires a corpus *declaration*, not the database's *current* state, and the migration corpus is only a model of the database that can go stale. `stash encrypt cutover` renames columns directly in the database without writing drizzle SQL, so the corpus can still describe a cut-over column as plaintext; the same is true of any hand-authored psql or Supabase-dashboard change. Reworded both to say the guarantee holds for what the corpus shows, not for a database that has drifted from its migration history.
…efixed near-misses A statement the strict matcher matched but skipped (already-encrypted or source-unknown) is left on disk unchanged, so it still contains `SET DATA TYPE` and the broad near-miss scan finds it again. When a `/* ... */` block comment sat glued to the front of that statement, STATEMENT_PREAMBLE_RE (which only stripped blank lines and `--` line comments) left the near-miss pass reporting a different statement string than the strict pass's `match.trim()`, so the dedup key never matched: the same statement was reported twice, once with the correct reason and once as `unrecognised-form` — contradictory advice (look for a hand-authored `USING` clause) for a statement that has none, with the block comment quoted back to the user besides. Extend the preamble regex to also strip a leading block comment so both passes agree on the statement text and the second report collapses into the first. Also tightens the `leaves an ALTER inside a NESTED block comment untouched` regression test to assert `skipped` is empty, not just `rewritten` — this branch's fail-closed change made the fixture's column undeclared, so the statement is skipped as source-unknown regardless of whether nested block comments are handled, and the old assertion passed either way. Also names, in the DECLARED_COLUMN_RE docstring, the dependency the "declared but not encrypted is plaintext by residue" claim has on MANGLED_TYPE_FORMS covering every encrypted shape: a domain installed into a non-`public` schema and an array of the domain both escape ENCRYPTED_TYPE_REF and get rewritten as if plaintext. Neither is a supported EQL layout or a drizzle-kit output, and both behaved this way before this branch, so this is documentation, not a regression. Verified the nested-block-comment guard is not vacuous: replacing the depth-tracking loop in isInsideCommentOrString with a plain first-`*/` scan makes the strengthened test fail (rewritten stays empty, but skipped now surfaces a source-unknown entry), confirming it exercises actual nesting behaviour rather than only checking `rewritten`.
…ed, not rewrote `unsafe = sweep.rewritten > 0 || sweep.skipped > 0` warned that the migration "DESTROYS data on any table that already holds rows" whenever anything was rewritten OR merely flagged. For a sweep that rewrote nothing and only flagged a source-unknown statement, that's false — nothing on disk changed, and the raw ALTER simply fails its cast at migrate time instead. This branch makes the pure-skip case the common one, so the overclaim needed its own message arm, matching the care already taken for the `unverified` (failed-sweep) case. Split `unsafe` into `destructive` (rewritten > 0) and `flaggedOnly` (nothing rewritten, but something was flagged), and give `flaggedOnly` a message that tells the truth: statements were flagged for review, nothing was rewritten, and the migration will fail at migrate time until they're resolved. The prompt still defaults to No in both cases. Also strengthens `defaults to No, and says why, when a file was rewritten` to assert the on-disk effect (the swept file contains DROP COLUMN, not SET DATA TYPE) rather than only the prompt defaulting to No and mentioning "DESTROYS data" — both of which the skipped-statement branch also produces, so the test could not tell a genuine rewrite from its `source-unknown` sibling. Confirmed by deleting the fixture's 0000_declare.sql (which makes the ALTER a source-unknown skip instead of a rewrite): the test now fails at the message assertion, since the flagged-only message added above no longer contains "DESTROYS data". Restored the fixture afterwards.
STATEMENT_PREAMBLE_RE anchored its block-comment group to `^` ahead of the line-comment loop, so it only stripped a leading block comment when that comment sat at the very start of the FILE. In any file with a preceding statement, NEAR_MISS_RE's match starts at that statement's `;` -- i.e. with a newline, not the comment -- so the anchored group could never match past it. The same statement was then reported twice: once correctly (already- encrypted or source-unknown), and once as unrecognised-form with the block comment glued to its front and guidance pointing at a hand-authored USING clause that isn't there. Fold the block comment into the repeating loop as its own alternative instead of a group ahead of it, so it can match after any number of leading newlines/line comments, in any interleaving. Hardens the existing regression test to use a preceding statement (the one shape the broken regex happened to handle, which is why it passed before), and adds coverage for an indented block comment on the ALTER's own line and for the already-encrypted reason. Each asserts the full skipped array, so both count and reason are pinned. Also documents the one known residue: a nested closed block comment ahead of a live ALTER still double-reports, because the alternative's lazy `*?` stops at the first `*/`. Accepted, not fixed. Verified by reverting the regex to the anchored form: the three new/ hardened tests fail with the described double-report in both packages, confirming they are load-bearing.
fix(cli,wizard): rewrite an encrypted ALTER only for a column the corpus declares
Code review of #780's content, retargeted hereThis review ran against 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 The headline is a cluster, not a listFindings 1–4 are all 1. Quote-parity drift makes a commented
|
Umbrella PR for #707 — the integration branch collecting the EQL v2 removal stack. Still a draft: seven of the planned PRs have landed,
packages/cliandpackages/stackstill carry v2, and the branch is one commit behindmain.Goal
Make EQL v3 the sole generation the workspace authors and emits, while preserving the ability to decrypt existing v2 payloads. "Remove v2" means remove the v2 emission and authoring surface, not the read path.
End state: no
eql_v2_*emission or authoring path, one naming convention (v3 takes the unsuffixed name), green suite, v2 ciphertext still readable.Decisions
Referenced by number from the sub-PRs. Stated here so they resolve.
test/live/side-by-side-clients-live-pg.test.tsruns a v2 and a v3 client in one process against one database. No shipped data-migration tooling.*V3name stays as a@deprecatedalias, so people already on v3 don't rewrite imports twice. Exception:stack-drizzle's./v3subpath is a hard break, no alias — post-collapse the root names match v2's, so a stale import would type-check while silently binding v3 semantics. Full rationale in Remove EQL v2 repo-wide (umbrella) #707's "Naming — decided".Landed
PRs 1–7. All green on
a16302fc.protect,schema,protect-dynamodb(~19k lines)migrate— drop EQL v2 from the domain-type classifierstackcore — v3 audit-on-decrypt, collapseEncryptionV3intoEncryptionstack-supabase— remove v2 authoring surface, de-suffix v3, deletequery-builder-v3.tsstack-drizzle— remove v2 root, collapse./v3to root (hard break, no alias)wizard— port the migration rewriter to the v3 domain familycli/examples/meta — de-suffix the init scaffold, rewriteexamples/basicto v3, sweep the docsVerified:
examples/basic,examples/prismaande2etypecheck clean; Biome reports 0 errors (196 warnings, CI gates on errors only); all eightSKILL.mdfiles are free of stale references, with every*V3mention explicitly marked deprecated;EncryptionErrorTypesand the EQL payload keys are untouched; everyexportsentry keeps bothimportandrequire.@cipherstash/stack/v3,EncryptionV3andencryptedSupabaseV3still exist as@deprecatedaliases per Decision 5 — they are not stale.Branch state
One commit behind
main, one conflict:packages/stack-drizzle/README.mdagainst #775. The sixstack-drizzleconflicts from #763 are already resolved —encryptedIndexessits atsrc/indexes.tsand is exported from the root barrel.Remaining
PR 8 —
cli+migrate, the v2 SQL leaf≈ −25,200 / +300 across ~40 files; ~2,700 lines excluding the vendored SQL.
Delete:
src/sql/*.sql(22,512 lines, read only byinstaller/index.tsand copied bytsup.config.ts:83);encrypt/cutover.ts(306);db/push.ts(177);db/activate.ts(73);db/supabase-migration.ts(133);migrate/src/eql.ts(166 — all eight functions confirmedeql_v2.*mutations, exported atmigrate/src/index.ts:44-53); the--eql-version/--latestflags atregistry.ts:353-364,426-437; the v2 routing indb/install.ts:269-316,697-727,760-816.eql upgradestays —db/upgrade.ts:29already defaults to v3. Only its flags go.Deleting
cutover.tsleaves no hole. The v3 ladder is complete:backfill.ts:561→drop.ts:113-123→dropped, already rendered byquest.ts:95-104. The identically-named plan step (parse-plan.ts:14,rollout-state.ts:26) is v3-valid and survives.Must survive:
db/status.ts:32-62,104-131(legacy v2 detection — without it a v2 customer is told "EQL is not installed"); the manifesteqlVersionfallbacks inencrypt/status.ts:127-129,154-183andstatus/index.ts:41,55,136-147(the only remaining v2 signal since #761);eql_v2_encryptedinrewrite-migrations.ts:11(that sweep runs on the v3 path over repos whose history carries v2 tokens);encrypt/lib/db-readers.ts:46-82; the empty-candidate guard inencrypt/lib/resolve-eql.ts.Decrypt-path risk, must be mitigated in the same PR: deleting the vendored SQL removes the only offline way to recreate
public.eql_v2_encryptedwhen restoring a v2 dump into a fresh Postgres. Surfaceinstaller/index.ts:38-40's upstreameql-2.3.1release URL in the--eql-version 2rejection message.Sequencing:
tsup.config.ts:83'scpSync('src/sql', …)throws on a missing directory, so it moves in the same commit. Confirmeql/migration.ts:229's ALTER COLUMN sweep is complete before deletinginstall.ts:638-680, or the CLI ships broken Drizzle migrations.Open: whether
--eql-version 3stays accepted (8 doc/skill sites plustest-kit/src/install.ts:64pass it;parseArgsis lenient, so leaving it silently ignored is dishonest).skills/stash-encryption/SKILL.md:1057becomes false on merge.PRs 9–12 —
packages/stack, the v2 authoring and emission surfaceNot previously tracked as outstanding, and the largest remaining piece.
src/schema/index.tsis 754 lines, of which ~511 (68%) are v2-only authoring:EncryptedColumn(269-452),EncryptedTable(459-548),EncryptedField(214-267),encryptedTable/encryptedColumn/encryptedField(599-716). All three root re-exports atsrc/index.ts:9are v2-only — the package root exposes no v3 authoring.The emission path is live.
resolveEqlVersion(encryption/index.ts:97-131) detects v3 byhasBuildColumnKeyMap, and:130returnsundefinedwhen no v3 table is present. protect-ffi 0.30.0 documentsundefinedas defaulting to 2. SoEncryption({ schemas: [v2Table] })still writes v2 wire, and there is no runtime rejection of a v2 schema (encryption/index.ts:879-947checks only non-empty schemas, keyset UUID, mixed sets and v2ste_vec). Only the WASM entry rejects, atwasm-inline.ts:1359-1366— the pattern to lift.Removing the DSL does not remove the
undefinedbranch: the looseBuildableTablepath (types.ts:271) would keep writing v2 silently.Test-coverage gap — Decision 6 is currently unmet. No test anywhere has a v3-configured client decrypt v2 ciphertext, which is the actual invariant customers depend on. All v2 coverage is same-client v2→v2 round-trip. Worse,
integration/shared/v2-decrypt-compat.integration.test.ts:29-45mints its fixture through the emission path being removed — it dies with the code it guards, and passes falsely until then. A purely static fixture is not viable as the sole replacement: ZeroKMS ciphertexts are keyset-bound, andtest-kit/src/env.ts:30-67lets credentials come from a developer's own profile, so a fixture minted against CI's workspace won't decrypt locally.Suggested split:
EncryptConfigrather than the DSL.buildEncryptConfig(encryption/index.ts:910currently uses the v2 copy for both generations); de-coupledynamodb/types.ts:11,29,266,280. No public surface change.encryptedTable/Infer*name collisions, port or drop ~30 v2-authored test files.Keep the
eqlVersion: 2hatch — it outlives the builders and the read-compat test needs it.Fix before merge
.changeset/carries 29 stale references across 12 files, and these render into the shipped 1.0.0 CHANGELOG.adapter-package-split.md:14,15,22-24andeql-v3-drizzle.md:5,8,12,13nameencryptedType,@cipherstash/stack/eql/v3/drizzleand the*V3drizzle symbols as migration targets; six further files point at@cipherstash/stack-drizzle/v3;drizzle-encrypted-indexes.md:5advertises the deleted/v3entry;eql-v3-sole-docs.md:10-13contradictsstack-dynamodb-v2-write-removal.mdon DynamoDB.packages/stack/README.md:446-451headlines the Supabase section with the deprecatedencryptedSupabaseV3, contradicting:766of the same file and three other sources.init/lib/introspect.ts:88matches onlyeql_v2_encrypted, sostash initpre-selects nothing on a v3 database and:233announces the wrong domain. Pre-existing onmain, fixed in PR 8.examples/*ande2e/*typecheck in no CI job —turbofilters to./packages/*. That is whyexamples/basiccould sit broken here. Add a job.Release mechanics
Mid-RC, fixed group:
stash,stack,stack-drizzle,stack-supabase,prisma-next,wizard(all at1.0.0-rc.4). Six major-bearing changesets land inside it —stack-1-0-0-rc,stack-audit-on-decrypt,stack-dynamodb-v2-write-removal,remove-eql-v2-drizzle-root,remove-eql-v2-supabase-authoring,supabase-single-row-typing— so the whole group goes to1.0.0-rc.5.migrate→1.0.0-rc.2;nextjs→4.1.2.Two corrections wanted:
remove-eql-v2-packages.md's@cipherstash/nextjs: patchis a phantom bump.packages/nextjsdepends only onjoseand references no@cipherstash/*package; it would ship a changelog entry about three packages it never used. Drop the line.remove-eql-v2-migrate-classifier.mdatpatchunder-communicates.migrate/src/version.ts:47-51dropped theeql_v2_encrypted → 2arm, solistEncryptedColumnssilently omits v2 columns under an unchanged signature, andresolveEncryptedColumn/detectColumnEqlVersioninherit it.minorat minimum (moot inside migrate's own major, but the changelog should say it).Out of scope
#708 (Supabase v3 in Workers/browser), #654 (v3 Supabase full-ciphertext scalar operand), #637, and customer data-migration tooling.
Closes
Closes #707
Closes #752
Closes #198
#198 closes as obsoleted, not implemented —
encryptedTypeno longer exists in the Drizzle public API, so the duplicative-generic complaint has no referent. #752's last item was thestash initscaffold, delivered by #776.Already closed as
NOT_PLANNEDon 2026-07-21, carried by this work: #409, #599, #423, #426, #421, #422.