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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions