Skip to content

Implement .withLockContext() on the wasm-inline entry #797

Description

@coderdan

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

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions