Conversation
…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 detectedLatest commit: 20ccab3 The changes in this PR will be included in the next version bump. This PR includes changesets to release 11 packages
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
marked this pull request as ready for review
October 1, 2026 02:14
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The encryption client in
@cipherstash/stack(the objectEncryption({ 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 12as nevercasts. 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.Jsoncolumn accepts a JSON document whose arrays can containnull, 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
packages/stack/src/encryption/operations/*,helpers/infer-index-type.ts): the encrypt, query, batch-query and bulk-encrypt operations acceptPlaintextInput(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 scatteredas JsPlaintextcasts.encryption/index.ts, private class):encryptQuerytakes anEncryptQueryArgstuple union, so a single value without options no longer type-checks. That call used to compile and then throw.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.src/types.ts): new internalPlaintextInput,QueryTermInput,BulkEncryptPayloadInputandEncryptQueryArgs. They are not added to the publictypesexport, so the publicPlaintext,ScalarQueryTermandBulkEncryptPayloadkeep their meaning.__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.@cipherstash/stackpatch (see Review notes).Verification
@cipherstash/stack:tsc --noEmitreports 0 errors insrc. The test and integration files show the same 142 errors as onmain.@cipherstash/stack:test:types68/68 passed (63 existing + 5 new).buildandtest:types:distpass.@cipherstash/stack:test— 1033 passed, 159 skipped, 10 files failed. All 10 fail at startup withCannot 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:typesandtest(371) pass.@cipherstash/stack-supabase:test:typesandtest(570) pass.tscfor stack-drizzle, stack-supabase andstash(the CLI): the same error sets asmain. 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 inclient-v3.ts.as never/as unknown/as any) inpackages/stack/src: 33 → 21. All 12 removed are fromclient-v3.ts; the remaining ones are in code this PR does not touch.Related
Closes #637
Review notes
EncryptOperation,EncryptQueryOperation,BatchEncryptQueryOperationandBulkEncryptOperationare public exports. Their constructor parameters andgetOperation()return types now include the JSON document type, which they already accepted at runtime. Nothing in this repo callsgetOperation()outside those classes. That is why there is a patch changeset. The alternative is to keepgetOperation()narrow, at the cost of an assertion inside each class.wasm-inline.tsstill has avalue as Plaintextthat may now be removable.toJsPlaintextcan be deleted once protect-ffi'sJsPlaintextallowsDateandnullarray elements. That should be a separate issue.client-v3.ts(theUnderlyingNativeClientdoc and the forwarding at the bottom), thenhelpers/js-plaintext.ts.