Skip to content

refactor(stack): remove the as-never forwarding casts in createEncryptionClient - #996

Open
tobyhede wants to merge 1 commit into
mainfrom
refactor/stack-remove-encryption-client-casts
Open

tobyhede wants to merge 1 commit into
mainfrom
refactor/stack-remove-encryption-client-casts

Conversation

@tobyhede

@tobyhede tobyhede commented Oct 1, 2026

Copy link
Copy Markdown
Contributor

Summary

The encryption client in @cipherstash/stack (the object Encryption({ schemas }) returns) gives each method precise per-column types, but internally it was built by forwarding every call to a loosely typed inner client through 12 as never casts. A cast like that switches the type checker off, so a mistake in the forwarding would have compiled silently. This PR removes those casts. Nothing changes at runtime.

The casts were hiding a real type gap: a types.Json column accepts a JSON document whose arrays can contain null, but the input type declared by the native module (@cipherstash/protect-ffi, the Rust core that does the encryption) does not allow that, even though the module accepts it. That gap is now closed in one place instead of being papered over at each call.

Changes

  • Operations (packages/stack/src/encryption/operations/*, helpers/infer-index-type.ts): the encrypt, query, batch-query and bulk-encrypt operations accept PlaintextInput (Plaintext | JsonDocument). A new helper, toJsPlaintext (helpers/js-plaintext.ts), is the one documented assertion where a value is handed to the native module. It replaces six scattered as JsPlaintext casts.
  • Native client (encryption/index.ts, private class): encryptQuery takes an EncryptQueryArgs tuple union, so a single value without options no longer type-checks. That call used to compile and then throw.
  • Typed client (encryption/client-v3.ts): forwards with no casts. The two model-encrypt methods each narrow their result with one visible, commented assertion, because the encrypted model's shape comes from walking the table at runtime and no type can derive it.
  • Types (src/types.ts): new internal PlaintextInput, QueryTermInput, BulkEncryptPayloadInput and EncryptQueryArgs. They are not added to the public types export, so the public Plaintext, ScalarQueryTerm and BulkEncryptPayload keep their meaning.
  • Comments rewritten so they claim only what the type checker actually checks.
  • Tests (__tests__/typed-client-v3.test-d.ts): type tests pinning the operation type each client method returns, and the decrypted model shape for the lock-context and bulk forms.
  • Changeset: @cipherstash/stack patch (see Review notes).

Verification

  • @cipherstash/stack: tsc --noEmit reports 0 errors in src. The test and integration files show the same 142 errors as on main.
  • @cipherstash/stack: test:types 68/68 passed (63 existing + 5 new). build and test:types:dist pass.
  • @cipherstash/stack: test — 1033 passed, 159 skipped, 10 files failed. All 10 fail at startup with Cannot find module …/protect-ffi-darwin-arm64/index.node, because the native binding was not built locally and no CipherStash credentials were set. The wrapper's own runtime tests (typed-client-v3.test.ts, client-get-schemas, v3-only-public-surface) pass. CI's credentialed jobs are the first real run of the encrypt paths.
  • @cipherstash/stack-drizzle: test:types and test (371) pass. @cipherstash/stack-supabase: test:types and test (570) pass.
  • tsc for stack-drizzle, stack-supabase and stash (the CLI): the same error sets as main. They come from test files only; main's set was taken by building with the old sources swapped in.
  • pnpm run code:check: clean, and Biome reports no type-erasing assertions left in client-v3.ts.
  • Type-erasing casts (as never / as unknown / as any) in packages/stack/src: 33 → 21. All 12 removed are from client-v3.ts; the remaining ones are in code this PR does not touch.

Related

Closes #637

Review notes

  • Small public type change. The issue aimed for no public API change. EncryptOperation, EncryptQueryOperation, BatchEncryptQueryOperation and BulkEncryptOperation are public exports. Their constructor parameters and getOperation() return types now include the JSON document type, which they already accepted at runtime. Nothing in this repo calls getOperation() outside those classes. That is why there is a patch changeset. The alternative is to keep getOperation() narrow, at the cost of an assertion inside each class.
  • Deferred: wasm-inline.ts still has a value as Plaintext that may now be removable. toJsPlaintext can be deleted once protect-ffi's JsPlaintext allows Date and null array elements. That should be a separate issue.
  • Start with client-v3.ts (the UnderlyingNativeClient doc and the forwarding at the bottom), then helpers/js-plaintext.ts.

…tionClient

createEncryptionClient built the typed EncryptionClient<S> by forwarding to
the native client through 12 type-erasing as-never casts. They hid a real
gap: v3 PlaintextForColumn includes the types.Json document, which the
operations' Plaintext input (built on the FFI's JsPlaintext) cannot express.

- Operation constructors accept PlaintextInput (Plaintext | JsonDocument);
  the single remaining plaintext assertion is toJsPlaintext at the FFI call,
  replacing six scattered as-JsPlaintext sites.
- Native encryptQuery takes an EncryptQueryArgs tuple union, so a scalar
  without options no longer type-checks, and the wrapper forwards unchanged.
- Model-encrypt results narrow to V3EncryptedModel with one visible,
  documented assertion each instead of a hidden cast.
- Comments rewritten so they claim only what the types actually check.

Closes #637
@changeset-bot

changeset-bot Bot commented Oct 1, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 20ccab3

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 11 packages
Name Type
@cipherstash/stack Patch
@cipherstash/bench Patch
stash Patch
@cipherstash/stack-drizzle Patch
@cipherstash/stack-prisma Patch
@cipherstash/stack-supabase Patch
@cipherstash/test-kit Patch
@cipherstash/basic-example Patch
@cipherstash/prisma-example Patch
@cipherstash/e2e Patch
@cipherstash/wizard Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@tobyhede
tobyhede marked this pull request as ready for review October 1, 2026 02:14
@tobyhede
tobyhede requested a review from a team as a code owner October 1, 2026 02:14
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-01T02:17:43.690095Z 20ccab3 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@tobyhede
tobyhede requested a review from freshtonic October 1, 2026 02:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Remove the as never forwarding casts in createEncryptionClient

1 participant