Ship .withLockContext() on the wasm-inline entry so identity-bound encryption works from both entries with the same call shape.
This is the implementation half of #793, which covers the diagnosis and the incorrect explanations currently in the source comment and the shipped skill. Filed separately so the code change can be tracked on its own; close or fold whichever is more convenient.
Current state
- Native:
.withLockContext({ identityClaim }) chainable on operations, composing with .audit() in either order.
- WASM: absent.
lockContext appears in packages/stack/src/wasm-inline.ts once, in a comment saying it is not there (:665).
Why it should be reachable
protect-ffi 0.30 accepts a lock context on both the single and bulk paths:
export type EncryptOptions = { plaintext; column; table; lockContext?: Context; unverifiedContext? }
export type EncryptPayload = { plaintext; column; table; lockContext?: Context }
export type Context = { identityClaim: string[] }
The WASM binding's opts are typed any (dist/wasm/protect_ffi.d.ts) because they cross serde into the same Rust core, so the shape is likely already accepted and simply not plumbed on the JS side. Confirm against the Rust first.
Note the stack's own EncryptOptions (types.ts:340) is { column, table } — no lockContext field — so nothing is being silently dropped today. The type just does not offer it.
Impact of leaving it
- Edge functions cannot write identity-bound data at all.
- Edge functions cannot read anything Node wrote under a lock context, since decrypt needs the same context. A silent split in what the two entries can read, on top of the schema nominal-typing incompatibility already documented in
skills/stash-edge.
- Supabase Edge Functions are the flagship WASM use case and the most likely place to want per-user key binding, since the user's JWT is already in hand.
Shape question to settle
The native surface is a chainable on a thenable operation. The WASM client's methods are async and return WasmResult directly (async encrypt(plaintext, opts): Promise<WasmResult<Encrypted>>), so a chainable requires returning a builder rather than a promise — a real change to the entry's shape. The alternatives are a per-call option, or converging both entries on one form.
Worth settling alongside #792, which raises the same "should these two entries have the same call shape" question for bulkEncrypt. Deciding them together avoids fixing the split in one place and widening it in another.
Coverage
The failure is silent — a wrong or missing claim surfaces as a failed decrypt, not a key error — so this needs round-trip tests: encrypt on one entry with a claim, decrypt on the other with the same claim; and the negative case, decrypt without it.
Docs that must follow
Ship
.withLockContext()on thewasm-inlineentry so identity-bound encryption works from both entries with the same call shape.This is the implementation half of #793, which covers the diagnosis and the incorrect explanations currently in the source comment and the shipped skill. Filed separately so the code change can be tracked on its own; close or fold whichever is more convenient.
Current state
.withLockContext({ identityClaim })chainable on operations, composing with.audit()in either order.lockContextappears inpackages/stack/src/wasm-inline.tsonce, in a comment saying it is not there (:665).Why it should be reachable
protect-ffi 0.30 accepts a lock context on both the single and bulk paths:
The WASM binding's
optsare typedany(dist/wasm/protect_ffi.d.ts) because they cross serde into the same Rust core, so the shape is likely already accepted and simply not plumbed on the JS side. Confirm against the Rust first.Note the stack's own
EncryptOptions(types.ts:340) is{ column, table }— nolockContextfield — so nothing is being silently dropped today. The type just does not offer it.Impact of leaving it
skills/stash-edge.Shape question to settle
The native surface is a chainable on a thenable operation. The WASM client's methods are
asyncand returnWasmResultdirectly (async encrypt(plaintext, opts): Promise<WasmResult<Encrypted>>), so a chainable requires returning a builder rather than a promise — a real change to the entry's shape. The alternatives are a per-call option, or converging both entries on one form.Worth settling alongside #792, which raises the same "should these two entries have the same call shape" question for
bulkEncrypt. Deciding them together avoids fixing the split in one place and widening it in another.Coverage
The failure is silent — a wrong or missing claim surfaces as a failed decrypt, not a key error — so this needs round-trip tests: encrypt on one entry with a claim, decrypt on the other with the same claim; and the negative case, decrypt without it.
Docs that must follow
wasm-inline.ts:665— currently says an auth strategy is identity-bound encryption. Wrong regardless of whether this lands.skills/stash-edge— already updated in feat(cli): add the stash-postgres and stash-edge skills — raw-SQL predicates and the WASM entry #777 in anticipation of this, including a code example that assumes the chainable form. If the API lands as a per-call option, that example needs correcting before feat(cli): add the stash-postgres and stash-edge skills — raw-SQL predicates and the WASM entry #777 merges.