Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
127 commits
Select commit Hold shift + click to select a range
4d92090
feat!: remove EQL v2-only published packages (protect, schema, protec…
tobyhede Jul 22, 2026
c40854b
docs: add EQL v2 removal PR1 plan
tobyhede Jul 22, 2026
24258a4
docs: correct plan changeset targets and stale-ref grep pattern
tobyhede Jul 22, 2026
4471471
refactor(migrate): drop EQL v2 from the domain-type classifier
tobyhede Jul 22, 2026
8832d35
feat(stack)!: EQL v3 audit-on-decrypt + collapse EncryptionV3 into En…
tobyhede Jul 22, 2026
5a0ed58
fix(stack): typed client tolerates nominal-arity decrypt calls
tobyhede Jul 22, 2026
f587d7b
refactor(stack-supabase): fold v3 query builder into base
tobyhede Jul 22, 2026
fdffe69
feat(stack-supabase)!: remove EQL v2 authoring surface, de-suffix v3 API
tobyhede Jul 22, 2026
a9a6b78
test(stack-supabase): update suites for folded v3 builder + de-suffix
tobyhede Jul 22, 2026
e0dea47
docs(stack-supabase): update stash-supabase skill + changesets for v2…
tobyhede Jul 22, 2026
8b69a34
docs(stack-supabase): clarify v2 decrypt path in changeset
tobyhede Jul 23, 2026
a3d6abb
refactor(stack-supabase): split query-builder monolith, ratchet FTA c…
tobyhede Jul 23, 2026
3d34862
feat(stack-drizzle)!: remove EQL v2 root and collapse ./v3 to root
tobyhede Jul 22, 2026
92a99e4
fix(bench): infer typed v3 client through a helper after Encryption c…
tobyhede Jul 23, 2026
3f0c616
ci(bench): install EQL v3 through the real CLI instead of relying on …
tobyhede Jul 23, 2026
fe0afa2
fix(bench,cli,docs): address PR review — dead JSON index, broken init…
tobyhede Jul 23, 2026
16c260f
docs(repo): repoint dead package paths + guard against recurrence
tobyhede Jul 23, 2026
b214b5f
refactor(cli): drop the unreachable v2 branch left by the classifier …
tobyhede Jul 23, 2026
87a9b00
chore(stack): address review nits
tobyhede Jul 23, 2026
90a1555
fix(stack): address PR 768 review — stale decrypt-audit guidance, sil…
tobyhede Jul 23, 2026
8ac4f64
fix(stack-supabase)!: type single()/maybeSingle() as one row, not an …
tobyhede Jul 23, 2026
42231e7
test(cli): retarget the codegen drizzle assertions at the collapsed root
tobyhede Jul 23, 2026
8cbe007
feat(wizard): port migration rewriter to the EQL v3 domain family
tobyhede Jul 22, 2026
761bdd9
fix(wizard,cli): sweep every migration dir; trim near-miss preamble
tobyhede Jul 23, 2026
0cef401
fix(wizard): resolve @cipherstash/auth via the node condition; gate t…
tobyhede Jul 23, 2026
9ed73a7
docs(skills): repoint drizzle index guidance at the collapsed root
tobyhede Jul 24, 2026
ccc7fbc
feat(cli,examples)!: de-suffix the init scaffold, rewrite examples/ba…
tobyhede Jul 24, 2026
b2f9d7a
docs(skills,meta): correct the naming the v2 removal inverted
tobyhede Jul 24, 2026
6c70e29
fix(wizard,cli): never rewrite a commented-out or already-encrypted A…
tobyhede Jul 24, 2026
38665ce
fix(stack)!: make Encryption's overloads agree with what it returns
tobyhede Jul 24, 2026
92a9141
fix(stack-supabase): export the single-row builder, drop dead v2 remn…
tobyhede Jul 24, 2026
d30882f
docs(skills,meta): stop describing EQL v2 as the current generation
tobyhede Jul 24, 2026
6c5a0e7
chore(ci,changesets): fix bench triggers, test-kit alias, overstated …
tobyhede Jul 24, 2026
6c3fb75
fix(wizard): default the migrate prompt to No when a sweep could not run
tobyhede Jul 24, 2026
ffe4974
fix(stack-supabase): accept interface row types, not just type aliases
tobyhede Jul 24, 2026
7353157
docs(skills): scope the v2 rollout lifecycle, and fix the drop column
tobyhede Jul 24, 2026
a5fab3c
docs: stop claiming the tooling detects a column's EQL v2 generation
tobyhede Jul 24, 2026
ed41b93
docs(changesets): fix claims that are false in the release they ship in
tobyhede Jul 24, 2026
8865866
fix(wizard,cli): treat quoted identifiers and string literals as inert
tobyhede Jul 24, 2026
2db9b42
fix(wizard,cli): an unterminated quoted identifier must fail safe too
tobyhede Jul 24, 2026
de804c2
fix(stack): only forward an EQL v3 table to the client on DynamoDB de…
tobyhede Jul 24, 2026
1e4056b
docs(stack-supabase): drop a comment about a removed code path
tobyhede Jul 24, 2026
c70da7c
docs: drop three more comments that explain the present via deleted code
tobyhede Jul 24, 2026
d46c454
docs(specs): fail-closed inversion for the encrypted ALTER COLUMN rew…
tobyhede Jul 24, 2026
46f30b4
fix(cli): rewrite an encrypted ALTER only for a column the corpus dec…
tobyhede Jul 24, 2026
034e804
fix(cli): close the keyword-mention gap and comment-check regression …
tobyhede Jul 24, 2026
923d6fb
fix(wizard): rewrite an encrypted ALTER only for a column the corpus …
tobyhede Jul 24, 2026
01ad43b
fix(cli): declare the column the fail-closed sweep now requires in mi…
tobyhede Jul 24, 2026
baa3163
fix(wizard): declare the column the fail-closed sweep now requires in…
tobyhede Jul 24, 2026
9c86403
docs(skills,changesets): the ALTER sweep repairs only what it can place
tobyhede Jul 24, 2026
e662407
docs(changesets,skills): scope the drop-ciphertext guarantee to what …
tobyhede Jul 24, 2026
bdb9414
fix(cli,wizard): collapse duplicate skip reports for block-comment-pr…
tobyhede Jul 24, 2026
9ce6c53
fix(wizard): stop claiming data destruction when the sweep only flagg…
tobyhede Jul 24, 2026
0fde344
fix(cli,wizard): fold the block-comment strip into the preamble loop
tobyhede Jul 24, 2026
d96020e
docs(specs): design for the Encryption signature fixes and typecheck …
tobyhede Jul 24, 2026
c94a5cc
fix(stack): give the typed client an init passthrough (A-7)
tobyhede Jul 24, 2026
fc13052
fix(stack)!: accept schema arrays that are not literals (A-4)
tobyhede Jul 24, 2026
031eab7
fix(stack,stack-drizzle,bench): name the client with EncryptionClient…
tobyhede Jul 24, 2026
ff93a62
ci: typecheck the surfaces that compiled nowhere, and cache dist (C-1)
tobyhede Jul 24, 2026
310bb19
docs(changesets,skills,prisma-next): record the signature changes
tobyhede Jul 24, 2026
9aa020e
ci: exempt bench from the dist outputs cache key
tobyhede Jul 24, 2026
d84ab1b
fix(cli,skills,stack-supabase): correct v2 cutover read guidance
tobyhede Jul 24, 2026
eb75e09
docs(changesets): drop the redundant lifecycle bullets
tobyhede Jul 24, 2026
856dcc8
fix(stack): make typed encryptQuery callable against the built package
tobyhede Jul 24, 2026
7faf16c
fix(stack): scope the dist type gate correctly
tobyhede Jul 24, 2026
36f9988
fix(stack): refuse EQL v2 wire over an all-v3 schema set (A-8)
tobyhede Jul 25, 2026
5d304ec
fix(stack): close the init wire-version back door, unbreak generic sc…
tobyhede Jul 27, 2026
4270884
fix(cli,wizard): detect non-public-schema and array EQL domains as en…
tobyhede Jul 25, 2026
af16e7b
test(cli,wizard): pin describeSkipReason's three reason mappings
tobyhede Jul 25, 2026
71d2d18
test(cli,wizard): cover the sweep's command and multi-directory layers
tobyhede Jul 25, 2026
3ab3925
fix(cli,wizard): index the rewrite's own target, and resolve the impl…
tobyhede Jul 25, 2026
cc84183
fix(cli,wizard): track dollar-quoted bodies and E'' escapes when scan…
tobyhede Jul 25, 2026
fe5750b
fix(cli,wizard): find the CREATE TABLE body terminator instead of gue…
tobyhede Jul 25, 2026
b954d91
test(scripts): compare the two rewriter copies so a one-sided fix fai…
tobyhede Jul 25, 2026
3d0de78
fix(wizard): sweep only drizzle-kit output directories
tobyhede Jul 25, 2026
184995a
fix(bench): key seed rows by JS property, and check it without a data…
tobyhede Jul 25, 2026
a9d430b
fix(stack): refuse the wasm-inline client for DynamoDB EQL v2 reads
tobyhede Jul 25, 2026
966978a
fix(stack): stop the wasm-inline client failing every DynamoDB v3 wri…
tobyhede Jul 27, 2026
3484411
fix(stack,bench): stop the client type asserting the .audit() it just…
tobyhede Jul 27, 2026
36e515a
fix(cli): stop encrypt cutover/drop acting on a guessed encrypted column
tobyhede Jul 25, 2026
31b9e69
fix(cli): make stash init's scaffolded client compile, and gate it
tobyhede Jul 25, 2026
04e0981
fix(cli): keep pure-v2 tables on the v2 ladder (#787 review)
tobyhede Jul 26, 2026
1b8cac2
fix(cli,migrate): close gaps found verifying the #787 review fixes
tobyhede Jul 27, 2026
1e837e5
test(ci): pin the turbo routing that the #787 review's minor finding …
tobyhede Jul 27, 2026
659423a
test(cli,ci): close the coverage and guard gaps found auditing #787
tobyhede Jul 27, 2026
ace2a4f
fix(skills,stack,scripts): correct shipped decrypt guidance, and make…
tobyhede Jul 25, 2026
79f3a53
fix(cli,scripts): per-integration query guidance, and linters that ca…
tobyhede Jul 25, 2026
0344e27
fix(cli,scripts): close the review nits, and the same defect where it…
tobyhede Jul 27, 2026
dc6dab6
fix(cli): stop the plan templates drafting a v2-only cutover on a v3 …
tobyhede Jul 27, 2026
ed2cc71
fix(cli): detect already-encrypted columns on EQL v3 during init
tobyhede Jul 27, 2026
ea74846
fix(stack): make adapter-kit importable without a process global
tobyhede Jul 26, 2026
41a04a6
fix(stack-supabase): recognise v3 columns structurally, not by class …
tobyhede Jul 26, 2026
1118efd
fix(stack-supabase): fail closed on unrecognised v3 column builders
tobyhede Jul 26, 2026
f9c785e
test(stack-supabase): pin the fail-closed message, not the [supabase …
tobyhede Jul 27, 2026
556072e
test(stack-supabase): unit-test the structural v3 probe, one member a…
tobyhede Jul 27, 2026
001a527
docs(stack): a new bare specifier is a signal, not automatically a de…
tobyhede Jul 27, 2026
8d31708
fix(stack,stack-supabase): diagnose an EQL v2 table instead of crashing
tobyhede Jul 27, 2026
17393b9
feat(cli): add the stash-postgres and stash-edge skills — raw-SQL pre…
coderdan Jul 28, 2026
5aed447
fix(scripts): stop the runners linter exiting 1 when a directory vani…
tobyhede Jul 28, 2026
19cff11
feat(cli): remove the EQL v2 migration leaf
tobyhede Jul 28, 2026
e26868d
fix(cli): remove stale v2 rollout guidance
tobyhede Jul 28, 2026
73e1483
test(cli): harden removed command regressions
tobyhede Jul 28, 2026
3a182bf
test(cli): cover all skill command runners
tobyhede Jul 28, 2026
ac9289e
fix(cli): finish EQL v2 retirement cleanup
tobyhede Jul 29, 2026
3a6395c
Merge pull request #824 from cipherstash/fix/script-test-race
tobyhede Jul 29, 2026
5b06d3e
fix(ci): sync EQL v3 migration guidance
tobyhede Jul 29, 2026
7d988c3
fix(cli): address review feedback
tobyhede Jul 29, 2026
78b9ab3
test(scripts): cover repository-linter race guards
tobyhede Jul 29, 2026
2d21deb
test(scripts): cover nested filesystem races
tobyhede Jul 29, 2026
487dc9b
fix(cli): distinguish missing encrypted column from legacy EQL v2
tobyhede Jul 29, 2026
9c7ff43
test(scripts): capture successful stderr output
tobyhede Jul 29, 2026
e155956
fix: finish v2 removal release gates
tobyhede Jul 29, 2026
7387414
test: close issue 816 release gaps
tobyhede Jul 29, 2026
9efecb2
Merge pull request #827 from cipherstash/fix/pr-824-review-feedback
tobyhede Jul 29, 2026
4dab29a
test(stack-supabase): cover multi-leaf or() substitution and not.and(…
tobyhede Jul 29, 2026
34c48eb
docs(stack): move the date-reconstruction rationale onto the function…
tobyhede Jul 29, 2026
d6ded7f
refactor(stack-supabase,skills): drop the unreachable csv transform, …
tobyhede Jul 29, 2026
fcb627b
perf(stack-supabase): reconstruct nested date paths in one pass
tobyhede Jul 29, 2026
673cc07
test(stack-drizzle): name the insert-row helper for what it actually …
tobyhede Jul 29, 2026
5b03135
test(e2e): assert CLI/wizard SKILL_MAP parity where both packages are…
tobyhede Jul 29, 2026
7f70879
docs(changeset): restore the encryptedSupabaseV3 alias, and say what …
tobyhede Jul 29, 2026
2db092d
docs(ci,stack-drizzle): keep the error counts and #778 in the gate co…
tobyhede Jul 29, 2026
0f54c0e
refactor(stack-supabase): collapse the or-string parser to the one pr…
tobyhede Jul 29, 2026
2b0eeb2
test(stack-supabase): pin parseLikeNeedle's escape handling directly
tobyhede Jul 29, 2026
c62d23c
docs(stack-supabase,changeset): mark csv() deprecated and state the d…
tobyhede Jul 29, 2026
ee33ac3
Merge pull request #828 from cipherstash/fix/issue-816
tobyhede Jul 29, 2026
bf7be09
Merge pull request #825 from cipherstash/fix/issue-814-remove-cli-mig…
tobyhede Jul 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
50 changes: 50 additions & 0 deletions .changeset/adapter-release-readiness.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
---
'@cipherstash/stack': minor
'@cipherstash/stack-supabase': minor
'stash': patch
'@cipherstash/wizard': patch
---

Finish the EQL v2-removal release gates and adapter correctness pass.

- **Supabase encrypts leaves nested inside a PostgREST boolean group.** This
is a disclosure fix, not a formatting one. The `.or()` string parser had
no group recursion, so `.or('and(createdAt.gte.2026-01-01,note.eq.x)')`
came back from the top-level split as one part and the leaf parser cut it
at the first dot into the pseudo-column `and(createdAt`. That name matched
no encrypted column, so the whole expression took the verbatim branch: the
operand `2026-01-01` reached PostgREST **as plaintext, against an
encrypted column**, under the JS property name `createdAt` rather than the
DB column name `created_at`. Every encrypted leaf nested inside `and(...)`
/ `or(...)` / `not.and(...)` leaked its operand to the database and
returned wrong results. Nested groups and `referencedTable` are now
preserved while each encrypted leaf is substituted in place.
- Supabase never sends nullish encrypted search operands as plaintext, honours
escaped LIKE metacharacters, rejects CSV result mode before decryption, and
diagnoses the removed object-form factory call. The bundled `stash-supabase`
skill no longer lists `csv()` among the transforms passed through to
Supabase — it throws, and the skill now says so and shows serializing the
decrypted rows instead.
- Native, WASM, and Supabase model decryption reconstruct valid date and
timestamp values consistently, including nested paths, aliases, and bulk
results, while leaving invalid values unchanged. That last clause is a
behavioural change on the native typed client and the Supabase adapter,
which previously pushed every date-like column through `new Date(...)`
unconditionally: a stored value that does not parse used to come back as an
Invalid `Date` and now comes back as the raw string, matching what the WASM
entry already did. The declared column type is still `Date`, so code that
assumed `instanceof Date` held for every date column — or called a `Date`
method on it unguarded, so that `.getTime()` used to yield `NaN` and now
throws a `TypeError` — has to handle the raw value.
- `stash init` names the concrete `public.eql_v3_*` domain family and gives
`public.eql_v3_text_search` as a valid Supabase example.
- CLI and wizard skill selection stay in parity for every integration,
including the Prisma Next skill, and verify that each selected skill has a
`SKILL.md`.

The final 1.0 integration surface is `Encryption` from
`@cipherstash/stack/v3`, the `@cipherstash/stack-drizzle` package root, and
`encryptedSupabase` from `@cipherstash/stack-supabase`. DynamoDB decrypt
operations retain `.audit()` on the typed `Encryption` client. Existing EQL v2
ciphertext remains readable through the core client; authoring and adapter
writes use EQL v3.
12 changes: 12 additions & 0 deletions .changeset/backfill-missing-column-message.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
'stash': patch
---

`stash encrypt backfill` now distinguishes a missing encrypted column from a
legacy EQL v2 one. The domain probe returns the same "not v3" answer for both,
so a user who had simply not added the `<col>_encrypted` column yet was told
they were on a legacy EQL v2 column and advised to migrate a domain that did not
exist. The command now reports that the column is absent, points at adding an
`eql_v3_*`-domain column and applying the migration, and mentions
`--encrypted-column` for non-standard names. The EQL v2 message is unchanged for
columns that really are present.
15 changes: 15 additions & 0 deletions .changeset/cli-near-miss-statement-trim.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
'stash': patch
---

Trim the leading comment block from near-miss statements reported by the Drizzle
migration rewriter (`stash eql migration --drizzle`, `stash eql install`).

The broad near-miss scan is anchored on the previous `;`, so a
`SET DATA TYPE … USING …` it could not safely repair was quoted back to the user
with every preceding comment and blank line glued to its front — in a file
opening with a comment block, that meant the whole header. The reported
statement is now the offending statement alone. Detection is unchanged; only the
text shown to the user is affected.

Keeps this rewriter in sync with its sibling in `@cipherstash/wizard`.
15 changes: 15 additions & 0 deletions .changeset/cli-v2-cutover-prompt-correction.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
'stash': patch
---

`stash init`'s setup prompt no longer tells agents to declare an EQL v2 cutover
column with a `types.*` domain.

The `types.*` factories are EQL v3 only — a v2 column is an `eql_v2_encrypted`
composite — so an agent following the old step-4 guidance would author a schema
that cannot describe the column it just cut over to. The prompt now says
explicitly not to use `types.*` for a v2 column, and points at the deprecated
`@cipherstash/stack/schema` builders with decryption through
`@cipherstash/stack`, which is the actual read path for legacy v2 rows.

Superseded later in this release: `stash encrypt cutover` and all v2 mutation guidance are removed; legacy v2 remains read-only.
73 changes: 73 additions & 0 deletions .changeset/decrypt-chaining-docs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
---
'stash': patch
'@cipherstash/stack': patch
---

Correct the shipped documentation for `decryptModel` / `bulkDecryptModels`.

Three places in `skills/stash-encryption` and four in `packages/stack/README.md`
said these return "a plain `Promise<Result<...>>` (not a chainable operation)"
and that there is therefore "no `.withLockContext()` to chain". They return an
`AuditableDecryptModelOperation`, which is thenable and carries both
`.withLockContext()` and `.audit()` — the same `.audit()` chain the
audit-on-decrypt work advertises. The skill contradicted itself: its own
reference table already listed the correct return type.

The skill ships inside the `stash` tarball and `installSkills()` copies it into
customer repos, so this was steering agents away from an API that exists. The
README ships in the `@cipherstash/stack` tarball.

The equivalent statement about the **WASM entry** is correct and unchanged —
`@cipherstash/stack/wasm-inline` really does return a plain promise from decrypt,
with no lock-context argument.

Also fixes the setup prompt `stash init` writes for coding agents, which
referenced `protectOps.eq` — an API that does not exist anywhere in the repo.
Every step naming an integration-specific API now branches on the project's
actual integration, instead of naming Drizzle's and Supabase's and leaving the
other two to guess:

- **Query paths.** `createEncryptionOperators(client)` (conventionally `ops`)
for Drizzle, the `encryptedSupabase` wrapper's own filters for Supabase, the
`eql*` column operators for Prisma Next, and `client.encryptQuery(...)` for a
plain Postgres project.
- **Schema authoring.** The `types.*` column factories for Drizzle, a concrete
`public.eql_v3_*` domain such as `public.eql_v3_text_search` in migration SQL
for Supabase, the `cipherstash.*`
field constructors in `schema.prisma` for Prisma Next, and `encryptedTable`
for plain Postgres. Prisma Next was previously sent at `types.*` /
`encryptedTable` — the client `stash schema build` explicitly refuses to
scaffold for that integration.
- **Read paths.** `decryptModel(row, usersSchema)` where that applies, and the
wrapper's transparent decryption where it does not.
- **Skill pointers.** A plain Postgres project installs no integration-specific
skill, so each "see the integration skill" was a pointer at a file that was
never written. Those now point at `stash-encryption`, which it does get.

`client.encryptQuery` is also shown taking the schema objects themselves
(`{ table: usersSchema, column: usersSchema.email }`) rather than an
object-shorthand that read as three required strings — `queryType` is inferred
from the column's configured indexes.

The cutover and complete-rollout **plan templates** now split EQL v3 from v2.
Both described the v2 rename swap (`<col>` → `<col>_plaintext`, twin → `<col>`)
as the only cutover path, so on the default v3 install `stash plan` drafted a
plan built around `stash encrypt cutover` — a command that refuses v3 columns
outright ("there is no rename step") and refuses entirely on a v3-only
database, where `eql_v2_configuration` does not exist. The implement prompt
already carried this split; the plan templates did not. The version is
per-column, so the templates tell the agent to establish it per column rather
than deciding once for the whole plan.

The "already encrypted" stop-and-ask now recognises `eql_v3_*` domains
alongside the legacy `eql_v2_encrypted` udt, so it can fire on the default
path at all.

**`stash init` now detects already-encrypted columns on EQL v3.** Database
introspection marked a column as CipherStash-managed only when its udt was
exactly `eql_v2_encrypted`. v3 columns carry per-domain types
(`eql_v3_text_search`, `eql_v3_integer_ord`, …), so on the default path every
encrypted column was reported as plaintext — shown with its `dataType` and left
unticked in the column picker, inviting a re-run to encrypt it a second time.
The picker also labelled any encrypted column with the literal string
`eql_v2_encrypted`; it now shows the column's real domain.
18 changes: 18 additions & 0 deletions .changeset/domain-carrier-not-public.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
'@cipherstash/stack': patch
---

The phantom domain carrier on encrypted v3 columns is no longer part of the
public surface.

Recovering a column's domain after declaration emit requires the type parameter
to appear bare somewhere in the instance type, which is what makes typed
`encryptQuery` callable against the published package. That carrier was a
plainly named `__domain` property, and this build has no `stripInternal`, so
`@internal` was documentation rather than enforcement: it appeared in
autocomplete on every column and could be read from consumer code, typed
`D | undefined` while being `undefined` at runtime in every case.

It is now keyed by a `unique symbol` that is not exported, so it is unnameable
outside the package while remaining exactly as inferrable. Nothing is emitted at
runtime and no call site changes.
2 changes: 1 addition & 1 deletion .changeset/drizzle-encrypted-indexes.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
'@cipherstash/stack-drizzle': minor
---

New `encryptedIndexes` helper on the `/v3` entry: spread
New `encryptedIndexes` helper on the package root: spread
`...encryptedIndexes(t)` in `pgTable`'s third-argument callback and it derives
the recommended functional indexes for every encrypted column in the table —
named `<table>_<column>_<capability>`, tracked by `drizzle-kit generate` like
Expand Down
24 changes: 13 additions & 11 deletions .changeset/dynamodb-eql-v3.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,17 @@
`encryptedDynamoDB` now accepts EQL v3 tables.

Pass a table built with `encryptedTable` + the `types.*` domains from
`@cipherstash/stack/v3` (or `@cipherstash/stack/eql/v3`) to any of
`encryptModel`, `bulkEncryptModels`, `decryptModel`, `bulkDecryptModels`. Both
the typed client from `EncryptionV3` and the nominal client from
`Encryption({ config: { eqlVersion: 3 } })` are accepted.
`@cipherstash/stack/v3` to any of `encryptModel`, `bulkEncryptModels`,
`decryptModel`, or `bulkDecryptModels`. Build the typed client with
`Encryption({ schemas: [table] })`.

EQL v2 tables continue to work unchanged — this is additive, and no existing
caller needs to change. The table decides which wire format is used, so a
DynamoDB table populated under one version must keep being read with that
version.
EQL v2 tables continue to be **readable** — `decryptModel` /
`bulkDecryptModels` still accept one, so existing items stay accessible. Writing
through a v2 table is a separate matter: `encryptModel` / `bulkEncryptModels`
narrowed to EQL v3 tables in this same release, so a caller that still encrypts
through a v2 table does need to change. The table decides which wire format is
used, so a DynamoDB table populated under one version must keep being read with
that version.

This fixes a latent bug that made v3 unusable: the write path detected an
encrypted value by its `k: 'ct'` tag, but EQL v3 scalars carry no `k`
Expand All @@ -33,8 +35,8 @@ Notes on capability:
path — `{ 'profile.ssn': types.TextEq('profile.ssn') }`. The model is
matched by dotted path, so `{ profile: { ssn } }` resolves, and the nested
attribute keeps its `__hmac` for key conditions.
- Audit metadata on `decryptModel` / `bulkDecryptModels` requires the nominal
client; the `EncryptionV3` client has no audit surface on decrypt.
- The typed `Encryption` client supports `.audit()` on `decryptModel` and
`bulkDecryptModels`, including when used through the DynamoDB adapter.

The DynamoDB adapter also gains its first test coverage — across the v2 and v3
paths, where it previously had none.
Expand Down Expand Up @@ -71,4 +73,4 @@ type, where a declared column `email` becomes `email__source` (plus
`email`. `decryptModel` / `bulkDecryptModels` invert it via `DecryptedAttributes`.
`AnyEncryptedTable`, `DynamoDBEncryptionClient` and `AuditConfig` are now
exported from `@cipherstash/stack/dynamodb` so these signatures can be named.
The EQL v2 overloads are unchanged.
The EQL v2 **decrypt** overloads are unchanged; the v2 encrypt overloads are removed in this release.
17 changes: 17 additions & 0 deletions .changeset/dynamodb-skill-wasm-caveat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
'stash': patch
---

The `stash-dynamodb` and `stash-encryption` skills documented EQL v2 decrypt as
unconditionally supported, without noting that `@cipherstash/stack/wasm-inline`
is EQL v3 only. Since that is the documented entry for Deno, Bun, Cloudflare
Workers and Supabase Edge Functions — runtimes commonly paired with DynamoDB — a
reader following the skill hit a runtime refusal with no forewarning. Both skills
now state that legacy v2 items are readable on the native `@cipherstash/stack`
entry only.

The `stash-dynamodb` API reference also claimed audit metadata forwards to
ZeroKMS "regardless of client shape". It does not: the wasm-inline client's
operations return a plain promise with no `.audit()`, so its audit metadata is
dropped (logged at debug). The reference now says so, and says the operation
still succeeds.
27 changes: 27 additions & 0 deletions .changeset/dynamodb-v2-read-table-forwarding.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
'@cipherstash/stack': patch
---

Fix `encryptedDynamoDB`'s EQL v2 read path, which failed against a v3-configured
client — the case the v2 read support exists for.

`decryptModel` and `bulkDecryptModels` forwarded the table to the encryption
client unconditionally. The second argument is only meaningful to the typed EQL
v3 client, which resolves it against its own reconstructor map; a legacy v2
table is not in that map, so the call failed with:

```
[eql/v3]: decryptModel received a table this client was not initialized with
```

That contradicted the adapter's documented contract — writes are EQL v3 only,
but decrypt keeps accepting a v2 table so previously stored items stay readable.
In practice the failure hit exactly the customers the compatibility promise was
written for: upgraded to a v3 schema, with v2 items still in the table.

The table is now forwarded only when it is an EQL v3 table. A v2 table takes the
table-less form, and the client derives the table from the payloads themselves.

Both paths are covered by a credential-free test asserting what the adapter
forwards, so a regression no longer depends on a live-credential integration run
to surface.
39 changes: 39 additions & 0 deletions .changeset/dynamodb-wasm-v2-read.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
---
'@cipherstash/stack': patch
---

`encryptedDynamoDB` now refuses a `@cipherstash/stack/wasm-inline` client paired
with a legacy EQL v2 table, instead of failing at the first read with a
misleading error.

The adapter's v2 read path deliberately calls `decryptModel(item)` with **no**
table — a v2 table means nothing to a v3 client's reconstructor map, and the
native clients derive the table from the payloads anyway. `WasmEncryptionClient`
cannot do that: its decrypt requires the table and resolves date fields from a
per-table map, so the omitted argument surfaced as
`TypeError: Cannot read properties of undefined (reading 'tableName')` thrown
from deep inside the client — on the documented entry for Deno, Cloudflare
Workers and Supabase Edge Functions, which satisfies the adapter's client type
structurally and so was accepted with no cast.

The pairing is now rejected at the call site, with a message naming both the
combination and the fix. The message is operation-neutral: the guard runs on all
four operations, so a plain-JS caller reaching the write path with a v2 table
gets a message that does not claim a read it never attempted.

`encryptModel` / `bulkEncryptModels` now also tolerate a client whose encrypt
returns a plain promise. They chained `.audit()` onto the result
unconditionally, which the wasm-inline client does not carry — so **every EQL v3
write through this adapter on that entry** failed with
`client.encryptModel(...).audit is not a function`, surfaced as a
`DYNAMODB_ENCRYPTION_ERROR` and so indistinguishable from a real encryption
fault. The read path already handled this; the write path now matches, via the
same fail-closed check that rejects a malformed result rather than passing an
unencrypted item through as a success. Audit metadata still has nowhere to go on
that client, so it is dropped and logged at debug — the write itself succeeds.

Three comments in this package claimed audit metadata was forwarded "regardless
of client shape" and that "every client this package ships carries `.audit()` on
decrypt". Neither was true of the wasm-inline client, whose decrypt returns a
plain promise — the metadata is dropped, observably (it is logged at debug), and
the comments and the debug message now say so.
20 changes: 20 additions & 0 deletions .changeset/encrypt-client-guard-parity.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
'stash': patch
---

`stash encrypt backfill` now names the cause when the encryption client has no
initialized encrypt config, instead of reporting a missing table.

The guard that refuses an unusable client file existed twice — once in
`loadEncryptConfig` (`stash db push` / `db validate`) and once, hand-copied, in
`loadEncryptionContext` (`stash encrypt backfill`). The copies had already
drifted: for a client whose `getEncryptConfig()` returns nothing, `db push`
exited 1 with `Encryption client in <file> has no initialized encrypt config`,
while `encrypt backfill` fell through to `Table "users" was not found in the
encryption client exports. Available: (none)` — naming the symptom rather than
the cause, which is precisely the failure the guard was added to eliminate.

Both loaders now call one shared guard, so a single file cannot produce two
different diagnoses, and the refusals are pinned at both public entry points.
The un-replaced `stash init` scaffold is unaffected — it was already refused by
both, with the same message.
Loading
Loading