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
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)fnbuild_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 (defaults customExportConditions to ['browser'])
Vite client builds (resolve.conditions defaults to ['module', 'browser', …])
webpack target: 'web' — i.e. every Next.js client bundle
Metro, but only for the web platform, 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 in packages/stack-supabase/package.json, pointing at the wasm build. There is no workerd condition to place it ahead of — none has ever existed.
Also give @cipherstash/stack a browser condition. stack-supabase/wasm-inline imports @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.ts and NON_NODE_RUNTIMES do not exist and never have.)
Add a typeof document refusal of config.accessKey. It belongs in packages/stack/src/wasm-inline.ts — probably resolveStrategy — not in stack-supabase, which has no makeClient function and no accessKey field. 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-ffi is a Node-API binary and no native client can exist in a browser at all.
Add browser to the runtime lists in the skill, README and AGENTS.md, and write a new changeset. (AGENTS.md:81 is 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, as e2e/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:
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.
Follow-up from the portable-
encryptedSupabasedesign (#708). Blocked on #804.Status: correct action today is to change nothing
The export map has no
browsercondition, and that is right. Without one, a browser bundler falls through toimport→dist/index.js→ the native binding → a loud build failure. Do not "fix" it.This was already decided once. Commit
054d4f4bnames thebrowserexport 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-ffiwas absorbed into this repo, so the blocker is a local file rather than an upstream unknown. This issue unblocks when thewasm32arm ofbuild_key_providerinpackages/protect-ffi/crates/protect-ffi/src/client_options.rsstops requiringsecret_key():#804 confirmed the requirement is real, is not a type over-declaration, and that
clientKeyis cryptographic key material. That is a Rust/protocol job incipherstash-suite, not a JS one here.Why the condition was cut
browseris matched beforeimport/requirewhen written first — Node's docs: "Within theexportsobject, key order is significant. During condition matching, earlier entries have higher priority."The blast radius is wider than originally written.
browseris set by:jest-environment-jsdom(defaultscustomExportConditionsto['browser'])resolve.conditionsdefaults to['module', 'browser', …])target: 'web'— i.e. every Next.js client bundle--platform=browser@rollup/plugin-node-resolve({ browser: true })webplatform, not by defaultOne 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
"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.@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.packages/stack-supabase/__tests__/wasm-entry-edge-safety.test.ts. (package-exports.test.tsandNON_NODE_RUNTIMESdo not exist and never have.)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".@cipherstash/protect-ffiis a Node-API binary and no native client can exist in a browser at all.clientKeyrequirement — the wasm arm errors regardless of strategy. That instruction would not work.AGENTS.md, and write a new changeset. (AGENTS.md:81is correct as-is today and needs no edit until this lands.)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.jsonalone, on the unmerged #810 branch:If that lands, update it — and note it does not cover
stack-supabaseat all.This is a breaking change
Adding an
exportskey 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 setsbrowser, the same import silently changes build: introspection disappears (schemasbecomes mandatory),withLockContext()andaudit()become throwers,bulkEncryptis not forwarded, andrequire()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/browseris 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.