Repository navigation
wasm-inline has no model helpers: encryptModel/decryptModel (and their bulk forms) are Node-only #742
Description
Summary
@cipherstash/stack/wasm-inline exposes no model operations at all. The native entry offers encryptModel / decryptModel / bulkEncryptModels / bulkDecryptModels; the WASM entry offers none of them, so edge code has to map every encrypted field by hand.
Verified in tree
$ grep -n "encryptModel\|decryptModel\|bulkEncryptModels\|bulkDecryptModels" packages/stack/src/wasm-inline.ts
(no matches)
The full WASM client surface today is encrypt, decrypt, isEncrypted, encryptQuery, encryptQueryBulk, and — as of #741 — bulkEncrypt / bulkDecrypt.
Why this is separate from #737
#737 asked for bulk values "and the *Models variants, for parity with the native entry". #741 implemented the value-level halves only, deliberately: there is no single-model operation on this entry to build a bulk one on top of. Adding bulkEncryptModels while encryptModel doesn't exist would be an incoherent surface, and the round-trip cost #737 was actually about is fully addressed by the value-level primitives.
So this is the remaining, and larger, half: port the model layer itself.
Impact
Model helpers are the ergonomic surface — they walk a model against its table schema, encrypt every declared column, and leave everything else alone. Without them, edge code does that traversal by hand for every read and write:
// Today, on the edge — manual, and easy to get wrong as the schema grows
const [email, bio] = await client.bulkEncrypt([
{ plaintext: user.email, table: users, column: users.email },
{ plaintext: user.bio, table: users, column: users.bio },
])
const row = { ...user, email, bio }
// What the native entry gives you
const row = await client.encryptModel(user, users)The failure mode is a quiet one: add a column to the schema and forget to add it to the hand-written mapping, and it silently persists in plaintext. The model helpers close that gap by construction, which is exactly why they matter more on a surface where people are hand-rolling the traversal.
Scope
encryptModel/decryptModel— the single-model traversal, ported to this entry.bulkEncryptModels/bulkDecryptModels— layered on those plus thebulkEncrypt/bulkDecryptprimitives added in feat(stack)!: align wasm-inline to the Result contract, and add bulkEncrypt/bulkDecrypt #741, so a list of models stays one ZeroKMS round trip.
Worth deciding up front which conventions to follow, since the WASM entry has deliberately diverged from the native one before (see #741): this surface throws rather than returning { data } | { failure }, and its bulk methods take plain index-aligned arrays rather than { id, … } envelopes. The model helpers should match the local surface for the same reason the bulk ones did — a WASM caller is reading this module's API, not the Node one.
Related
- feat(stack)!: align wasm-inline to the Result contract, and add bulkEncrypt/bulkDecrypt #741 — added
bulkEncrypt/bulkDecrypthere; its class docblock records the model gap as a separate port. - wasm-inline has no bulkDecrypt/bulkEncrypt: an N-row list is N ZeroKMS round-trips (rc.3 M1) #737 — the originating rc.3 finding (M1).
- wasm-inline has no auto/dev-profile auth strategy — credential-free local dev impossible on WASM #663 — the other open WASM gap: not credential-free.
Activity
docs(stack): move the date-reconstruction rationale onto the function…