You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
dist/wasm-inline.js imports the NODE @cipherstash/protect-ffi, defeating the entry's purpose #744
The built WASM entry imports both protect-ffi entries:
$ grep -oE 'from "@cipherstash/protect-ffi[^"]*"' packages/stack/dist/wasm-inline.js | sort -u
from "@cipherstash/protect-ffi"
from "@cipherstash/protect-ffi/wasm-inline"
The bare specifier is the Node entry, which loads the native NAPI binding. @cipherstash/stack/wasm-inline exists precisely so that never happens — its docblock opens with "for Deno, Bun, Cloudflare Workers, Supabase Edge Functions, and any runtime where the native @cipherstash/protect-ffi NAPI bindings are unavailable."
Pre-existing
Confirmed by building main directly — not introduced by #741. Recording it because that PR touched the entry and the check was worth doing.
Why it may not be biting yet
Whether this breaks at runtime depends on the consumer's bundler and whether the import is tree-shaken or hoisted. The supabase-worker example and the Deno WASM E2E both pass today, so on those paths it evidently survives. That makes it more insidious rather than less: the entry currently relies on downstream tree-shaking to uphold a guarantee it states unconditionally, and a consumer whose bundler behaves differently gets a native-binding load on an edge runtime.
Likely source
Value (not type-only) imports reaching the Node entry transitively. One concrete example: packages/stack/src/encryption/helpers/error-code.ts does
which is a runtime class reference, so any module importing getErrorCode drags the Node entry in. Worth auditing the whole import graph of src/wasm-inline.ts rather than assuming that is the only one.
Related consequence: getErrorCode narrows via instanceof against the NodeProtectError, and the WASM build exports no such class — so failure.code is likely always undefined on the WASM path even once the import is gone.
Nothing guards it
packages/prisma-next has test/bundling-isolation.test.ts asserting its entries stay separable; packages/stack has no equivalent for the WASM entry. A test asserting dist/wasm-inline.js contains no bare @cipherstash/protect-ffi import would have caught this at the point it was introduced, and would keep it caught.
Fix
Audit src/wasm-inline.ts's transitive imports for anything reaching the Node protect-ffi; make them type-only, or provide WASM-local equivalents (as WasmPlaintext already does for JsPlaintext).
Add the bundling-isolation test, modelled on the prisma-next one.
Decide what failure.code should mean on the WASM path, given there is no ProtectError class there.
Correction: this WAS introduced by #741, and is now fixed there
I filed this claiming the native import was pre-existing. That was wrong, and the verification behind it was invalid.
I had run git stash -u to swap in main, but the tree was already clean — so it stashed nothing, and I rebuilt my own branch and compared it against itself. Both builds were the same build.
Re-verified properly by swapping in main's src/wasm-inline.ts and rebuilding in place:
with #741's wasm-inline.ts: @cipherstash/protect-ffi
@cipherstash/protect-ffi/wasm-inline
with main's wasm-inline.ts: @cipherstash/protect-ffi/wasm-inline
The cause was #741's own import { getErrorCode } from '@/encryption/helpers/error-code' — a value-import of the native ProtectError class for an instanceof narrow. main never had it.
Replaced with a structural readErrorCode, which is the only thing that could ever have worked on that path anyway: the WASM build ships no error class, so instanceof never matches there regardless.
Also added packages/stack/__tests__/wasm-inline-bundle-isolation.test.ts, which asserts the built bundle's external imports — the guard this issue asked for. Verified it fails when the regression is reintroduced (2 of its 3 assertions), rather than assuming it would.
Closing
Nothing pre-existing remains: main was clean, #741 broke it, #741 fixes it, and CI now guards it. Closing as resolved-in-#741.
Two related points did survive into their own issue — #746 — since they are genuinely pre-existing: the .d.ts still references type-only symbols from the native protect-ffi root, and failure.code semantics on the WASM path.
Activity
fix(stack): address review — native FFI leak, Result plumbing, batch …
fix(stack): address review — native FFI leak, Result plumbing, batch …
coderdan commentedon Jul 21, 2026
Correction: this WAS introduced by #741, and is now fixed there
I filed this claiming the native import was pre-existing. That was wrong, and the verification behind it was invalid.
I had run
git stash -uto swap inmain, but the tree was already clean — so it stashed nothing, and I rebuilt my own branch and compared it against itself. Both builds were the same build.Re-verified properly by swapping in
main'ssrc/wasm-inline.tsand rebuilding in place:The cause was #741's own
import { getErrorCode } from '@/encryption/helpers/error-code'— a value-import of the nativeProtectErrorclass for aninstanceofnarrow.mainnever had it.Fixed in #741
Replaced with a structural
readErrorCode, which is the only thing that could ever have worked on that path anyway: the WASM build ships no error class, soinstanceofnever matches there regardless.Also added
packages/stack/__tests__/wasm-inline-bundle-isolation.test.ts, which asserts the built bundle's external imports — the guard this issue asked for. Verified it fails when the regression is reintroduced (2 of its 3 assertions), rather than assuming it would.Closing
Nothing pre-existing remains:
mainwas clean, #741 broke it, #741 fixes it, and CI now guards it. Closing as resolved-in-#741.Two related points did survive into their own issue — #746 — since they are genuinely pre-existing: the
.d.tsstill references type-only symbols from the native protect-ffi root, andfailure.codesemantics on the WASM path.feat(stack)!: align wasm-inline to the Result contract, and add bulkE…