Skip to content

Consolidate the three hand-rolled encryption-client views into @cipherstash/stack/adapter-kit #807

Description

@tobyhede

Follow-up from the portable-encryptedSupabase design (#708).

Rewritten and downgraded 2026-08-26 after an audit. The original described a design that was never built: AdapterEncryptionClient, fromNativeEncryptionClient, fromWasmEncryptionClient, assertClientShape and supportsLockContext have zero matches across all 31,837 objects in the git database. The portable-runtime series shipped a different shape (#912, 1a3e9808, 2026-08-19). Original text preserved in the issue history.

What actually exists

Six structural views, not three. Every one erases operands so a test double needs no cast:

Type Location Members
UnderlyingNativeClient packages/stack/src/encryption/client-v3.ts:59-80 10 — encrypt, encryptQuery, encryptModel, bulkEncryptModels, decrypt, decryptModel, bulkDecryptModels, bulkEncrypt, bulkDecrypt, getEncryptConfig
CipherstashV3Client packages/stack-prisma/src/v3/sdk-adapter-v3.ts:74-89 bulkEncrypt, decrypt, bulkDecrypt, encryptQuery
DynamicEncryptionClient packages/stack-supabase/src/query-encrypt.ts:55-68 encrypt, encryptModel, bulkEncryptModels
DynamoDBEncryptionClient / CallableEncryptionClient packages/stack/src/dynamodb/types.ts:50-55, 79-96 encryptModel, bulkEncryptModels, decryptModel, bulkDecryptModels
OperandEncryptionClient packages/stack-drizzle/src/operators.ts:74-80 encryptQuery only
EncryptionClientLike packages/migrate/src/backfill.ts:69-84 bulkEncryptModels

The Supabase seam is adaptWasmEncryption (packages/stack-supabase/src/wasm-client-adapter.ts:79), which returns an EncryptionFactory — not a client view at all. packages/stack/src/adapter-kit.ts is 68 lines and still declares no client interface.

Why this is now LOW, and harder than it looked

"The three method sets are disjoint" is false. Drizzle's {encryptQuery} is a strict subset of Prisma's. What survives is weaker: no view is a superset of the other two.

A superset already exists — but it will not serve as the donor. UnderlyingNativeClient contains every member of every other view. Two problems:

  1. It is a superset of member names, not an assignability supertype. Return types genuinely diverge. Prisma returns PromiseLike<StackResult<…>>; UnderlyingNativeClient returns chainable operation classes; DynamoDB's members return unknown on purposedynamodb/types.ts:66-78 records that declaring a chainable shape there "failed EVERY EQL v3 write" on the wasm entry (fix(stack,bench): wasm-inline DynamoDB v2 reads, and a bench seed that never encrypted (#772 review findings 10, 12) #788). These are different projections, not narrower copies.
  2. It is module-private on purpose, and packages/stack/__tests__/typed-client-v3.test.ts:8 asserts that. Promoting it is a public-surface decision, not a refactor.

So consolidation is blocked on reconciling return types across two engines — the actual hard part, and unaddressed by the original framing. There is still no second consumer of the port: only stack-supabase has two entries.

The fourth "duplication" is not one. hasBuildColumnKeyMap (packages/stack/src/types.ts:251-259) probes one member on a table. isV3ColumnLike (packages/stack-supabase/src/column-map.ts:57-70) probes four on a column, and its own comment at :46-48 explains why four. Consolidating them would be wrong.

The two folded-in items

Item 1, the entry/arity check — obsolete, do not do it. options.encryptionClient was removed; passing the old form is a hard error at packages/stack-supabase/src/create.ts:252-256, naming it "the removed EQL v2 API". The engine binds at the entry point, so a native client cannot reach the WASM entry by construction. The supporting claim was also backwards: decryptModel.length does discriminate — 1 native (encryption/index.ts:466-468), 2 wasm (wasm-inline.ts:1250-1253) — merely inverted relative to bulkEncrypt (2 native, 1 wasm). Any arity probe must be per-method. The third data point is gone: packages/stack/src/encryption/v3.ts is now a 6-line re-export shim.

Item 2, the lock-context hardcode — mostly fixed; split the residual out. The named harm is closed: withUnsupportedChainers (wasm-client-adapter.ts:58-67) throws a named error citing #797 rather than a raw TypeError. What remains is a taxonomy gap — it throws a plain Error, not an EncryptionFailedError, so error.encryptionError stays undefined at packages/stack-supabase/src/query-builder.ts:599-616 while error.message carries the full explanation. A caller branching on that field, which the comment at :608-609 documents as the pattern, sees nothing. Roughly a five-line fix and it needs none of this refactor.

Scope if pursued

adapter-kit.ts (an audit decision per its own header, line 18), wasm-inline.ts, client-v3.ts, stack-drizzle/src/operators.ts:74 and its test-d, stack-prisma/src/v3/sdk-adapter-v3.ts:74, stack-supabase/wasm-client-adapter.ts and query-encrypt.ts:55.

Potentially breaking: CipherstashV3Client is a published type on two @cipherstash/stack-prisma entries (src/exports/stack.ts:33, src/exports/v3.ts:77). Moving or renaming it is a major there unless a deprecated alias stays.

Changesets: @cipherstash/stack minor, @cipherstash/stack-prisma major-or-patch, stack-drizzle and stack-supabase patch, plus stash patch — skills/stash-encryption/SKILL.md:197 documents the adapter-kit subpath.

Corrections to the original text

  • packages/prisma-next/packages/stack-prisma/. The former has zero tracked files today (it was a real package before the rename; the directory is now build residue), as are packages/drizzle, protect, protect-dynamodb, schema, stack-forge.
  • Drop the AdapterEncryptionClient row and the claim that the Adapter… name was chosen so this becomes a file move. No such name was chosen; this is now a rename.
  • Line fixes: adapter-kit.ts 60→68 lines, header quote at 18; operators.ts 67-73→74-80; types.ts 276→251-259; encryption/index.ts 651→601; wasm-inline.ts 944→1040; query-builder.ts 596-598→599-616. Delete v3.ts:367 — that surface no longer exists.
  • Three in-tree comments cite the moved hasBuildColumnKeyMap wrongly, and disagree with each other: adapter-kit.ts:63 says types.ts:268-275, column-map.ts:40 says 276-283, column-map.ts:139 says 276. Actual: 243-259.

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions