Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
65940c3
test(stack): characterise encryptedDynamoDB v2 behaviour
tobyhede Jul 20, 2026
d26950d
feat(stack): accept EQL v3 tables in encryptedDynamoDB
tobyhede Jul 20, 2026
84b8865
fix(stack): type the DynamoDB storage payload structurally
tobyhede Jul 20, 2026
35b155f
docs(skills): sharpen the v2/v3 nested-object distinction
tobyhede Jul 20, 2026
ffa7e3a
docs: nested fields in v3 are deferred, not decided against
tobyhede Jul 20, 2026
440caa6
docs: correct the v3 nested-field framing again — it is a regression
tobyhede Jul 20, 2026
02e75f5
feat(stack): support nested v3 attributes in encryptedDynamoDB
tobyhede Jul 20, 2026
09604fd
fix(stack): correct four data-integrity bugs found in review
tobyhede Jul 20, 2026
c32720d
feat(stack): strongly type the DynamoDB v3 surface + property tests
tobyhede Jul 20, 2026
7105c5c
fix(stack): second-review fixes for DynamoDB v3 adapter
tobyhede Jul 20, 2026
6368ce2
docs(skills): fix CodeRabbit findings in stash-dynamodb, pin __hmac d…
tobyhede Jul 21, 2026
997a4c2
docs(skills): clarify v2/v3 produce the same layout, not the same item
tobyhede Jul 21, 2026
5e72ae2
docs: fix stale numbers flagged in review (numRuns, test count)
tobyhede Jul 21, 2026
5b26ad3
fix(stack): address DynamoDB v3 review findings with regression tests
tobyhede Jul 21, 2026
75b1558
fix(stack): adapt DynamoDB v3 adapter to protect-ffi 0.30 SteVec format
tobyhede Jul 21, 2026
ebddb2c
fix(stack): close five DynamoDB v3 review-body findings with tests
tobyhede Jul 22, 2026
a4e6312
test(stack): close two DynamoDB v3 audit gaps + harden test flakiness
tobyhede Jul 22, 2026
57de926
test(stack): fail client-compat type tests on Result failure
tobyhede Jul 22, 2026
4161657
test(stack): fail the live v3 DynamoDB suite loudly on missing creds
tobyhede Jul 22, 2026
23b677a
refactor(stack): align DynamoDB isStoredEqlPayload with core predicate
tobyhede Jul 22, 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
74 changes: 74 additions & 0 deletions .changeset/dynamodb-eql-v3.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
---
'@cipherstash/stack': minor
'stash': patch
---

`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.

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.

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`
discriminator at all. Every v3 scalar fell through to the nested-object branch
and was written as a raw map instead of being split into `<attr>__source` and
`<attr>__hmac`.

Notes on capability:

- Only equality is usable on DynamoDB. `<attr>__hmac` is written for domains
that mint an `hm` term — the `*Eq` family, plus `TextOrd`/`TextOrdOre`/
`TextSearch`. Ordering and bloom-filter terms have no DynamoDB query surface
and are not stored, so those columns remain decryptable but not queryable.
- Nested attributes are supported in v3. There is no nested-group authoring
form (that is a compile error), so declare the column flat with a dotted
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 DynamoDB adapter also gains its first test coverage — across the v2 and v3
paths, where it previously had none.

Robustness, from review:

- Passing a v3 table to a client that never registered it (one built for a
different schema set, so it is not in v3 mode for that table) now throws a
clear, actionable error naming the table, instead of failing opaquely deep in
the FFI.
- A malformed decrypt result from a non-conforming client is surfaced as a
failure rather than resolving as a silent `undefined` success.
- Reading back a `<attr>__source` attribute that matches no declared column now
logs a debug diagnostic instead of silently returning the raw ciphertext.
- Caller input that cannot be structurally cloned no longer reaches the FFI by
reference — the "encryption never mutates a caller's object" guarantee holds
on that path too.
- The write path now splits only declared columns, matched on the same property
path the read path rebuilds from. A pre-encrypted payload placed under an
undeclared nested name is stored whole (and round-trips) instead of being
split into a `<attr>__source` the read path could never reassemble.
- A degenerate payload with an empty-string ciphertext is split like any other
ciphertext rather than falling through and being written as a raw map, which
had leaked its `v`/`i` envelope metadata into storage.
- Arrays are documented as a deliberate carve-out: the mapping does not descend
into them, so a payload inside a list is stored whole (still decryptable, but
not queryable and not part of the `__source`/`__hmac` layout).

The v3 overloads are strongly typed. `encryptModel` / `bulkEncryptModels` check
the input model against the table's column domains, and return the DynamoDB
attribute map that is actually written — the new exported `EncryptedAttributes`
type, where a declared column `email` becomes `email__source` (plus
`email__hmac` for the equality domains that mint one) rather than surviving as
`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.
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ If these variables are missing, tests that require live encryption will fail or
- `packages/stack-supabase`: Supabase integration (`@cipherstash/stack-supabase`), depends on `@cipherstash/stack` — `encryptedSupabase` (v2) and `encryptedSupabaseV3` (v3). Split out of `@cipherstash/stack`.
- `packages/schema`: Schema builder utilities and types (`encryptedTable`, `encryptedColumn`, `encryptedField`)
- `packages/nextjs`: Next.js helpers and Clerk integration (`./clerk` export)
- `packages/protect-dynamodb`: DynamoDB helpers (`encryptedDynamoDB`)
- `packages/protect-dynamodb`: DynamoDB helpers (`protectDynamoDB`), built on `@cipherstash/protect`. A fork of the shipping adapter, kept for existing consumers of the standalone package; EQL v2 only. The maintained implementation is `packages/stack/src/dynamodb` (`encryptedDynamoDB`)
- `packages/utils`: Shared config (`utils/config`) and logger (`utils/logger`)
- `packages/bench`: Performance / index-engagement benchmarks (private, not published)
- `e2e/*`: Cross-package end-to-end tests (package managers, supply chain, Prisma example README)
Expand Down Expand Up @@ -147,7 +147,7 @@ Three rules to remember when editing CI or pnpm config:
## Key Concepts and APIs

- **Initialization**: `EncryptionV3({ schemas })` returns the typed EQL v3 client. (`Encryption({ schemas })` is the legacy v2 client.) Provide at least one `encryptedTable`.
- **Schema (EQL v3, the documented approach)**: Define tables/columns with `encryptedTable` and the `types.*` concrete-domain factories from `@cipherstash/stack/eql/v3` (`types.TextSearch`, `types.IntegerOrd`, `types.Json`, …) — each domain's query capabilities are fixed by its type; there are no chainable capability tuners. Build the client with `EncryptionV3` from `@cipherstash/stack/v3`. (Legacy EQL v2 — `Encryption` + `encryptedColumn(...).equality().freeTextSearch().orderAndRange().searchableJson()` from `@cipherstash/stack/schema` — remains for existing deployments and is what DynamoDB (`encryptedField`) still requires; see #657.)
- **Schema (EQL v3, the documented approach)**: Define tables/columns with `encryptedTable` and the `types.*` concrete-domain factories from `@cipherstash/stack/eql/v3` (`types.TextSearch`, `types.IntegerOrd`, `types.Json`, …) — each domain's query capabilities are fixed by its type; there are no chainable capability tuners. Build the client with `EncryptionV3` from `@cipherstash/stack/v3`. (Legacy EQL v2 — `Encryption` + `encryptedColumn(...).equality().freeTextSearch().orderAndRange().searchableJson()` from `@cipherstash/stack/schema` — remains for existing deployments. DynamoDB accepts both v3 and v2 tables; nested fields work in both — v2 via `encryptedField` groups, v3 via a flat dotted column path (`'profile.ssn': types.TextEq(...)`), since `EncryptedV3TableColumn` admits no nested groups.)
- **Operations** (all return Result-like objects and support chaining `.withLockContext(lockContext)` and `.audit()` when applicable):
- `encrypt(plaintext, { table, column })`
- `decrypt(encryptedPayload)`
Expand Down
Loading
Loading