Repository navigation
Add the browser export condition to @cipherstash/stack-supabase, with a live browser smoke test #805
Description
Follow-up from the portable-encryptedSupabase design (#708). Blocked on #804.
Rewritten 2026-08-26 after an audit. The original text was written against a design draft that never merged, and named three files, constants and functions that have never existed in this repository — verified by scanning all 31,837 objects in the git database, not just reachable refs. Those steps are removed below. The original is preserved in the issue history.
Status: correct action today is to change nothing
The export map has no browser condition, and that is right. Without one, a browser bundler falls through to import → dist/index.js → the native binding → a loud build failure. Do not "fix" it.
This was already decided once. Commit 054d4f4b names the browser export condition (#805), a live browser smoke test and browser guidance as deliberately out of scope. The condition was proposed and rejected in writing. This issue re-opens that decision; it is not unstarted work.
The block, now checkable in-tree
packages/protect-ffi was absorbed into this repo, so the blocker is a local file rather than an upstream unknown. This issue unblocks when the wasm32 arm of build_key_provider in packages/protect-ffi/crates/protect-ffi/src/client_options.rs stops requiring secret_key():
#[cfg(target_arch = "wasm32")]
pub(crate) fn build_key_provider(&self) -> Result<SecretKey, Error> {
self.secret_key().ok_or_else(|| {
Error::Credentials(
"clientOpts.clientId and clientOpts.clientKey are required — this build has no \
profile store to fall back to".to_string(),
)
})
}#804 confirmed the requirement is real, is not a type over-declaration, and that clientKey is cryptographic key material. That is a Rust/protocol job in cipherstash-suite, not a JS one here.
Why the condition was cut
browser is matched before import/require when written first — Node's docs: "Within the exports object, key order is significant. During condition matching, earlier entries have higher priority."
The blast radius is wider than originally written. browser is set by:
jest-environment-jsdom(defaultscustomExportConditionsto['browser'])- Vite client builds (
resolve.conditionsdefaults to['module', 'browser', …]) - webpack
target: 'web'— i.e. every Next.js client bundle - esbuild
--platform=browser - Rollup
@rollup/plugin-node-resolve({ browser: true }) - Parcel
- Metro, but only for the
webplatform, not by default
One narrowing correction: Vitest + jsdom is not reliably affected. Vite 6+ resolves those environments through SSR conditions, which exclude browser (vitest-dev/vitest#8431, #7070 — both closed, #8431 as won't-fix, so the behaviour persists).
So the realistic failure is not "a jsdom test resolves oddly". It is a WASM crypto blob and a clientKey-shaped API silently entering a production client bundle.
Checklist when it lands
- Add
"browser"as the first key of the"."entry inpackages/stack-supabase/package.json, pointing at the wasm build. There is noworkerdcondition to place it ahead of — none has ever existed. - Also give
@cipherstash/stackabrowsercondition.stack-supabase/wasm-inlineimports@cipherstash/stack/wasm-inline; a condition on the leaf alone leaves the graph resolving through a browser-less parent. - Add the runtime assertion to
packages/stack-supabase/__tests__/wasm-entry-edge-safety.test.ts. (package-exports.test.tsandNON_NODE_RUNTIMESdo not exist and never have.) - Add a
typeof documentrefusal ofconfig.accessKey. It belongs inpackages/stack/src/wasm-inline.ts— probablyresolveStrategy— not instack-supabase, which has nomakeClientfunction and noaccessKeyfield. Nothing was removed from that package; there is no guard to "reinstate". - Give the refusal a remedy that is not "pass a native client" — not because an adapter cannot take one, but because
@cipherstash/protect-ffiis a Node-API binary and no native client can exist in a browser at all. - Do not write the OIDC-federation instruction the original step 5 called for. WasmClientConfig requires clientKey on every auth arm, so no browser-safe wasm client can be constructed #804 proved federation does not lift the
clientKeyrequirement — the wasm arm errors regardless of strategy. That instruction would not work. - Add browser to the runtime lists in the skill, README and
AGENTS.md, and write a new changeset. (AGENTS.md:81is correct as-is today and needs no edit until this lands.) - Add the live browser smoke test. Genuinely greenfield — there is no playwright, puppeteer, karma, jsdom or happy-dom anywhere in the repo, and no
vite.config.*. It must load the real inlined WASM, ase2e/wasm/does under Deno.
Guard that must not regress
The original said the series ships a grep asserting no positive browser claim exists in any shipping artifact. It does not. No such guard exists on main. The nearest thing is a two-line JSON assertion scoped to packages/stack/package.json alone, on the unmerged #810 branch:
expect(packageJson.browser).toBeUndefined()
expect(JSON.stringify(packageJson.exports)).not.toContain('"browser"')If that lands, update it — and note it does not cover stack-supabase at all.
This is a breaking change
Adding an exports key is normally additive. It is not when the key sorts ahead of existing keys and redirects an already-resolving specifier to a different artifact. For anyone whose tooling sets browser, the same import silently changes build: introspection disappears (schemas becomes mandatory), withLockContext() and audit() become throwers, bulkEncrypt is not forwarded, and require() stops working because the wasm entry is ESM-only. Same specifier, same range, different behaviour, no type error.
Prefer a named subpath. @cipherstash/stack-supabase/browser is opt-in, breaks nobody, and makes the major bump unnecessary. A major bump does not discharge the duty here: a consumer bumping a major expects to read a changelog, not to have their client bundle quietly acquire a WASM crypto blob.
Activity
test(stack): pin the WASM clientKey contract, document the entry as s…
test(stack): pin the WASM clientKey contract, document the entry as s…
docs: stop advertising the WASM entry as browser-capable (#804)