Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
994e5b2
feat!: remove EQL v2-only published packages (protect, schema, protec…
tobyhede Jul 22, 2026
689021e
docs: add EQL v2 removal PR1 plan
tobyhede Jul 22, 2026
27a0915
docs: correct plan changeset targets and stale-ref grep pattern
tobyhede Jul 22, 2026
a88fac4
refactor(migrate): drop EQL v2 from the domain-type classifier
tobyhede Jul 22, 2026
686004f
feat(stack)!: EQL v3 audit-on-decrypt + collapse EncryptionV3 into En…
tobyhede Jul 22, 2026
04f1942
fix(stack): typed client tolerates nominal-arity decrypt calls
tobyhede Jul 22, 2026
2b4e2e9
refactor(stack-supabase): fold v3 query builder into base
tobyhede Jul 22, 2026
f5f07a8
feat(stack-supabase)!: remove EQL v2 authoring surface, de-suffix v3 API
tobyhede Jul 22, 2026
3bde0c1
test(stack-supabase): update suites for folded v3 builder + de-suffix
tobyhede Jul 22, 2026
b5cdede
docs(stack-supabase): update stash-supabase skill + changesets for v2…
tobyhede Jul 22, 2026
d6d8978
chore(stack): address review nits
tobyhede Jul 23, 2026
6e167d3
docs(stack-supabase): clarify v2 decrypt path in changeset
tobyhede Jul 23, 2026
6af8955
refactor(stack-supabase): split query-builder monolith, ratchet FTA c…
tobyhede Jul 23, 2026
3d69a59
fix(stack-supabase)!: type single()/maybeSingle() as one row, not an …
tobyhede Jul 23, 2026
9fc0f39
docs(repo): repoint dead package paths + guard against recurrence
tobyhede Jul 23, 2026
8bb0270
Merge pull request #760 from cipherstash/worktree-feat+remove-eql-v2-…
tobyhede Jul 23, 2026
b3a3499
refactor(cli): drop the unreachable v2 branch left by the classifier …
tobyhede Jul 23, 2026
93286ad
fix(stack): address PR 768 review — stale decrypt-audit guidance, sil…
tobyhede Jul 23, 2026
0d706e9
Merge pull request #761 from cipherstash/feat/remove-eql-v2-pr2-migrate
tobyhede Jul 23, 2026
b52e17b
Merge pull request #768 from cipherstash/feat/remove-eql-v2-pr3-stack…
tobyhede Jul 23, 2026
fecb883
Merge pull request #769 from cipherstash/feat/remove-eql-v2-pr4-supabase
tobyhede Jul 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions .changeset/pre.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,6 @@
"@cipherstash/migrate": "0.2.0",
"@cipherstash/nextjs": "4.1.1",
"@cipherstash/prisma-next": "0.3.2",
"@cipherstash/protect": "12.0.1",
"@cipherstash/protect-dynamodb": "12.0.1",
"@cipherstash/schema": "3.0.1",
"@cipherstash/stack": "0.19.0",
"@cipherstash/stack-drizzle": "0.0.0",
"@cipherstash/stack-supabase": "0.0.0",
Expand Down Expand Up @@ -70,7 +67,6 @@
"remove-legacy-drizzle-package",
"remove-secrets-leftovers",
"rename-db-install-to-eql-install",
"schema-stevec-standard-pin",
"skills-eql-v3-accuracy",
"skills-identity-docs-refresh",
"stack-1-0-0-rc",
Expand Down
20 changes: 20 additions & 0 deletions .changeset/remove-eql-v2-migrate-classifier.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
'@cipherstash/migrate': patch
---

Drop EQL v2 from the domain-type classifier. `classifyEqlDomain` (and the
`detectColumnEqlVersion` / `listEncryptedColumns` / `resolveEncryptedColumn`
resolution built on it) no longer recognise the legacy `eql_v2_encrypted`
domain — v3 is the sole generation this workspace authors and backfills, so a
column's version is now determined solely from its self-describing `eql_v3_*`
domain type. A legacy v2 column's version is carried by the manifest's recorded
`eqlVersion` instead (the CLI's `encrypt status` / `status` renderers already
fall back to it), so status output is unchanged for v2 columns already recorded
in `.cipherstash/migrations.json`. A v2 column backfilled from here on records
no `eqlVersion` and so reports no version in `stash encrypt status` — the v2
lifecycle itself (cut-over, then dropping `<column>_plaintext`) is unaffected.

This removes v2 *classification*, not the v2 read path: existing v2 ciphertext
remains decryptable through `@cipherstash/stack`. `EqlVersion` keeps its `2`
member for manifest-sourced legacy values; the exported function signatures are
unchanged.
22 changes: 22 additions & 0 deletions .changeset/remove-eql-v2-packages.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
'@cipherstash/stack': patch
'@cipherstash/nextjs': patch
---

Remove the EQL v2-only published packages `@cipherstash/protect`,
`@cipherstash/schema`, and `@cipherstash/protect-dynamodb` from the repository
and the release train. They formed a closed dependency chain
(`@cipherstash/protect-dynamodb` → `@cipherstash/protect` → `@cipherstash/schema`)
and are superseded by `@cipherstash/stack`:

- `@cipherstash/protect` (core encryption) → `@cipherstash/stack`, which now
carries the encryption client directly.
- `@cipherstash/schema` (schema builders) → `@cipherstash/stack/schema`.
- `@cipherstash/protect-dynamodb` (standalone DynamoDB adapter) →
`@cipherstash/stack/dynamodb` (`encryptedDynamoDB`), the maintained
implementation.

Already-published versions remain installable from npm; the git history
preserves the source for any emergency maintenance. Existing EQL v2 ciphertext
stays decryptable through `@cipherstash/stack` — this removes the v2 authoring
and emission surface, not the read path.
45 changes: 45 additions & 0 deletions .changeset/remove-eql-v2-supabase-authoring.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
---
'@cipherstash/stack-supabase': major
---

Remove the EQL v2 authoring surface and de-suffix the v3 API to the canonical
unsuffixed names (part of the EQL v2 removal, #707).

- **`encryptedSupabase` is now the connect-time-introspecting EQL v3 factory**
(formerly `encryptedSupabaseV3`). `encryptedSupabaseV3` remains as a
type-identical `@deprecated` alias, so existing imports keep working.
- **The legacy v2 `encryptedSupabase({ encryptionClient, supabaseClient })`
wrapper is removed** — with it the two-argument `from(tableName, schema)` form
and the hand-written client-side v2 schema. Its `EncryptedSupabaseConfig` and
the v2 `EncryptedSupabaseInstance`/`EncryptedQueryBuilder` type shapes are gone;
the unsuffixed type names now denote the v3 surface.
- **The `*V3` type exports are de-suffixed** to their canonical names —
`EncryptedSupabaseV3Options` → `EncryptedSupabaseOptions`,
`EncryptedSupabaseV3Instance` → `EncryptedSupabaseInstance`,
`TypedEncryptedSupabaseV3Instance` → `TypedEncryptedSupabaseInstance`,
`EncryptedQueryBuilderV3` → `EncryptedQueryBuilder`,
`EncryptedQueryBuilderV3Untyped` → `EncryptedQueryBuilderUntyped`,
`V3FilterableKeys` → `FilterableKeys`, `V3OrderableKeys` → `OrderableKeys`, and
the rest of the `*V3` key-helper types. Each keeps a type-identical
`@deprecated` `*V3` alias.

**Reading existing v2 data.** Only the v2 *authoring/emission* surface is removed
— no v2 ciphertext is stranded. Decryption in `@cipherstash/stack` is
generation-agnostic, so EQL v2 payloads still decrypt through the core client
(`decrypt` / `decryptModel`). This adapter, however, is now EQL v3 only and will
not auto-read an `eql_v2_encrypted` column: to read legacy v2 data during
migration, decrypt fetched rows with `@cipherstash/stack` directly, or run a
v2-configured setup alongside the v3 one and route per column. Mixed-generation
handling is a customer-side concern (install both and handle it explicitly), not
adapter auto-detection.

Internally the v3 query builder (`query-builder-v3.ts`) was folded into the base
`EncryptedQueryBuilderImpl`, which is now natively EQL v3; no runtime behaviour or
wire encoding changed.

**Migration:** rename `encryptedSupabaseV3` → `encryptedSupabase` (or keep using
the alias). If you still use the v2 `encryptedSupabase({ encryptionClient,
supabaseClient }).from(table, schema)` wrapper, migrate the table to an
`eql_v3_*` column domain and switch to the introspecting factory —
`await encryptedSupabase(supabaseUrl, supabaseKey)` — see the `stash-supabase`
skill and https://cipherstash.com/docs.
11 changes: 11 additions & 0 deletions .changeset/remove-eql-v2-supabase-skill.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
'stash': patch
---

Update the bundled `stash-supabase` agent skill for the EQL v2 removal (#707):
`encryptedSupabase` is now the connect-time-introspecting EQL v3 factory (with
`encryptedSupabaseV3` kept as a `@deprecated` alias), and the legacy v2
`encryptedSupabase({ encryptionClient, supabaseClient })` authoring wrapper has
been removed. The skill's examples, exported-type list, and migration/cutover
guidance are corrected accordingly. Skills ship inside the `stash` tarball, so
the stale v2 guidance would otherwise land in a user's project.
8 changes: 0 additions & 8 deletions .changeset/schema-stevec-standard-pin.md

This file was deleted.

41 changes: 41 additions & 0 deletions .changeset/stack-audit-on-decrypt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
---
'@cipherstash/stack': major
---

The typed EQL v3 client's `decryptModel` / `bulkDecryptModels` are now
audit-chainable. They return a chainable operation (a `MappedDecryptOperation`)
instead of a bare `Promise<Result<…>>`, so you can attach audit metadata and a
lock context before awaiting:

```typescript
await client
.decryptModel(item, table)
.audit({ metadata: { requestId } })
.withLockContext({ identityClaim: ["sub"] })
```

Both chaining orders forward the metadata to ZeroKMS, and the `Date`
reconstruction still applies to the successful result. Await-only call sites are
unchanged — the operation is still thenable to the same `Result`. This restores
audited decrypt through the DynamoDB adapter (`encryptedDynamoDB(...).decryptModel`)
for a v3 client, which previously had nowhere to carry the metadata.

Chaining `.withLockContext()` onto a decrypt operation that already took a lock
context positionally (`decryptModel(item, table, lc).withLockContext(other)`) now
throws instead of silently keeping the first. Pass the lock context one way or
the other, not both.

**Breaking:** `EncryptionV3` is now a deprecated, type-identical alias of
`Encryption`: `Encryption` is overloaded so an array of concrete EQL v3 tables
yields the same strongly-typed client. Use `Encryption` for new code. If you were
already passing EQL v3 tables to plain `Encryption`, you now receive the typed
client rather than the nominal one — its `decryptModel` / `bulkDecryptModels`
return type changes, and the two-argument form reconstructs `Date` columns from
`cast_as` instead of leaving them as ISO strings. Code that read those columns as
strings needs updating. As part of this collapse
`EncryptionV3` no longer independently pins the wire format — like `Encryption`,
it now honours an explicit `config.eqlVersion` (the retained migration escape
hatch). The `eqlVersion` config field and the `@cipherstash/stack/schema` EQL v2
builders remain available (now marked `@deprecated`) for reading and migrating
legacy v2 data; the client authors EQL v3 only. Their full removal is deferred to
a later PR.
17 changes: 17 additions & 0 deletions .changeset/stack-dynamodb-v2-write-removal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
'@cipherstash/stack': major
---

**Breaking (DynamoDB adapter):** `encryptedDynamoDB(...).encryptModel` and
`bulkEncryptModels` no longer accept an EQL v2 table — write is EQL v3 only. The
v2 write type overloads have been removed, narrowing encrypt to `AnyV3Table`.

**Decrypt still reads existing v2 items.** `decryptModel` / `bulkDecryptModels`
continue to accept an EQL v2 table (`encryptedColumn` / `encryptedField` from
`@cipherstash/stack/schema`), so previously stored v2 DynamoDB items remain
readable — the adapter keeps its v2 envelope reconstruction. Only the v2 write
surface is gone.

Migrate v2 write call sites to an EQL v3 table (`encryptedTable` + `types.*` from
`@cipherstash/stack/eql/v3`). To keep reading old data, pass the v2 table to the
decrypt methods.
13 changes: 13 additions & 0 deletions .changeset/stack-skills-eql-v3-audit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
'stash': patch
---

Skills refresh for the EQL v3 collapse (ships in the `stash` tarball):

- `stash-dynamodb`: audited decrypt now works on the typed client —
`client.decryptModel(item, table).audit({ … })` — so the old "use
`Encryption({ config: { eqlVersion: 3 } })` for audited decrypts" caveat is
removed. Encrypt/write is EQL v3 only; decrypt still reads existing v2 items.
- `stash-encryption`: canonical examples use `Encryption` (with `EncryptionV3`
noted as a deprecated alias); the DynamoDB notes state encrypt is v3-only while
decrypt still reads v2.
32 changes: 32 additions & 0 deletions .changeset/supabase-single-row-typing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
---
'@cipherstash/stack-supabase': major
---

`single()` and `maybeSingle()` now type `data` as the ROW, not an array.

Both have always returned one object at runtime, but the builder kept
advertising the array shape it was created with, so `data` was typed `T[] | null`
while holding a single row. Every caller had to launder it:

```typescript
const { data } = await supabase.from('users').select('id, email').single()
// before: data is `User[] | null` — wrong; a cast was the only way through
const user = data as unknown as User
// after: data is `User | null`
data?.email
```

`single()`/`maybeSingle()` now return `EncryptedSingleQueryBuilder<T>`, which
awaits to `EncryptedSupabaseResponse<T>` (`data: T | null`). That covers the
zero-row case for `maybeSingle()` and the error case for both, so no separate
null modelling was needed.

Filters and transforms are not chainable after `single()`/`maybeSingle()`,
matching supabase-js — applying one afterwards would change the query the
single-row promise was made about. `returns<U>()` preserves the awaited shape,
so `.single().returns<U>()` still awaits one row.

**Migration:** delete the cast. Code that worked around the old typing with
`data as unknown as Row` (or read `data![0]`) should now use `data` directly;
the cast still compiles but is no longer needed, and `data![0]` becomes a type
error.
8 changes: 5 additions & 3 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,11 @@ updates:
- dependency-name: "@cipherstash/auth-*"
# Release-managed manually alongside stack releases. Grouped bumps are
# actively harmful here: Dependabot's "group consistency" once upgraded
# the sunsetting packages/protect off its 0.23.0 pin (0.24+ renames the
# exports it imports) while DOWNGRADING the stack packages from 0.29.0
# (see #673).
# the (since-removed) `protect` package off its 0.23.0 pin — 0.24+ renamed
# the exports it imported — while DOWNGRADING the stack packages from
# 0.29.0 (see #673). The surviving consumers (stack, stack-drizzle,
# stack-supabase) pin one version between them and must stay in lockstep,
# so the rule stands.
- dependency-name: "@cipherstash/protect-ffi"
# 0.x bumps ship breaking type changes (e.g. 0.2 → 0.3 tightened the
# FailureOption constraint). Review and apply manually.
Expand Down
17 changes: 13 additions & 4 deletions .github/workflows/fta-v3.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,19 @@ jobs:
# blocking gate. No `continue-on-error`.
# One step per package so a failure names the offending package. Each caps
# at its current worst file (a ratchet, not an aspiration): stack src/eql/v3
# at 69, and the split adapter packages at their monolith maxima — drizzle
# 89 (operators.ts), supabase 91 (query-builder.ts). Lower a cap whenever a
# file is refactored below the next threshold; the v2 query-builder/operators
# monoliths are the debt to whittle down toward stack's 69.
# at 69, drizzle 89 (operators.ts, still a monolith), and supabase 71
# (query-encrypt.ts at 70.12, after the query-builder monolith was split
# across column-map / query-encrypt / query-mutation / query-dbspace /
# query-filters / query-results). Lower a cap whenever a file is refactored
# below the next threshold; drizzle's operators.ts is the remaining debt to
# whittle down toward stack's 69.
#
# NB supabase's top three now cluster tightly (query-encrypt 70.12,
# query-builder 70.05, helpers 69.13), so 71 is a ~0.9 margin. A cap set
# flush against its max is fragile — the 91 this replaced was 0.12 above
# its max and a single refactor blew straight through it. If a formatting
# reflow trips this step without a real complexity increase, re-measure
# (`npx fta src --format table`) before assuming the code got worse.
- name: Analyze stack (eql/v3) complexity
run: pnpm --filter @cipherstash/stack run analyze:complexity

Expand Down
2 changes: 0 additions & 2 deletions .github/workflows/integration-drizzle.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ on:
# suite that would catch it.
- 'packages/stack/src/encryption/**'
- 'packages/stack/src/schema/**'
- 'packages/schema/**'
- 'packages/stack/integration/**'
# The WASM family suite (integration/wasm/**) exercises this entry:
- 'packages/stack/src/wasm-inline.ts'
Expand All @@ -46,7 +45,6 @@ on:
# suite that would catch it.
- 'packages/stack/src/encryption/**'
- 'packages/stack/src/schema/**'
- 'packages/schema/**'
- 'packages/stack/integration/**'
# The WASM family suite (integration/wasm/**) exercises this entry:
- 'packages/stack/src/wasm-inline.ts'
Expand Down
2 changes: 0 additions & 2 deletions .github/workflows/integration-prisma-next.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ on:
# suite that would catch it.
- 'packages/stack/src/encryption/**'
- 'packages/stack/src/schema/**'
- 'packages/schema/**'
- 'packages/test-kit/**'
- 'packages/cli/src/installer/**'
- 'local/docker-compose.postgres.yml'
Expand All @@ -45,7 +44,6 @@ on:
# suite that would catch it.
- 'packages/stack/src/encryption/**'
- 'packages/stack/src/schema/**'
- 'packages/schema/**'
- 'packages/test-kit/**'
- 'packages/cli/src/installer/**'
- 'local/docker-compose.postgres.yml'
Expand Down
2 changes: 0 additions & 2 deletions .github/workflows/integration-supabase.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ on:
# trigger the live suite that would catch it.
- 'packages/stack/src/encryption/**'
- 'packages/stack/src/schema/**'
- 'packages/schema/**'
- 'packages/stack/integration/**'
- 'packages/test-kit/**'
- 'packages/cli/src/installer/**'
Expand All @@ -38,7 +37,6 @@ on:
# trigger the live suite that would catch it.
- 'packages/stack/src/encryption/**'
- 'packages/stack/src/schema/**'
- 'packages/schema/**'
- 'packages/stack/integration/**'
- 'packages/test-kit/**'
- 'packages/cli/src/installer/**'
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/rebuild-docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name: Rebuild Docs
on:
push:
tags:
- '@cipherstash/protect@*'
- '@cipherstash/stack@*'

jobs:
trigger-docs-rebuild:
Expand Down
Loading
Loading