Skip to content

[medium] SDK packaging blocks browser consumers: CJS-only barrel pulls Veil + WASM into every import #39

Description

@nandal

Two requests from building a browser client against @bytesbrains/maktub-sdk@0.1.0-dev.4. The first shapes how everything else gets consumed, so it's worth deciding before the second.

1. Packaging: a browser consumer cannot import the crypto without the Veil/WASM barrel

The package is CommonJS-only — main: dist/index.js, with no module field, no exports map, and no "type": "module". The barrel entry pulls everything unconditionally:

dist/index.js:122        require("./veil/veil.js")
dist/veil/veil.js:62     require("./wasm/warden_wasm.js")

So

import { deriveReadingKeyFromPrfOutput } from "@bytesbrains/maktub-sdk";

evaluates the Veil module and its WASM glue as a side effect of importing one HKDF function. Rollup/esbuild can't reliably tree-shake CommonJS, so it ends up in the bundle.

Why this matters more than bundle size

A browser client that derives reading keys wants the tightest possible CSP on that origin, because the derived key is long-lived and its published half is write-once — one injected script that reads it decrypts everything that user has ever received, unrotatably. script-src 'self' with no 'unsafe-eval' and no 'wasm-unsafe-eval' is achievable for a static bundle.

WebAssembly instantiation requires wasm-unsafe-eval. So if the WASM actually instantiates on import, a consumer either weakens the policy protecting the key-derivation origin, or can't use the SDK's crypto at all.

I've confirmed the glue is required at module-evaluation time; whether the .wasm binary itself instantiates eagerly or lazily depends on warden_wasm.js and I haven't measured that. Either way the structural problem stands: there is currently no way to ask for the crypto alone.

Request

An ESM build alongside CJS, and an exports subpath map, so consumers can import narrowly:

{
  "type": "module",
  "exports": {
    ".":        { "types": "./dist/index.d.ts",        "import": "./dist/index.js",        "require": "./dist/index.cjs" },
    "./crypto": { "types": "./dist/crypto/index.d.ts", "import": "./dist/crypto/index.js", "require": "./dist/crypto/index.cjs" },
    "./veil":   { "types": "./dist/veil/veil.d.ts",    "import": "./dist/veil/veil.js",    "require": "./dist/veil/veil.cjs" }
  }
}

@bytesbrains/maktub-sdk/crypto would then give derivation and ECIES with no Veil, no WASM, and no CSP implications. A "sideEffects": false flag would help bundlers further.

If a full dual build is too much for now, even an ESM build with the Veil re-export moved off the root barrel would solve the immediate problem.

2. Four pieces of smart-wallet client logic that don't exist in the SDK yet

Also missing, and each is a place where two language ports must agree byte-for-byte or a user lands on the wrong wallet / the chain rejects the signature:

Piece Consequence of divergence
Counterfactual address derivation — CREATE2 over the ERC-1167 creation code with the implementation spliced in A wrong address is an account that cannot be found, and a reading key published against it is stranded
P-256 public-key recovery from an ECDSA signature (SEC 1 §4.1.6) Used to identify which credential signed an assertion; a mismatch in the candidate set breaks credential-based account discovery
WebAuthn codecs — DER signature parse, attestation-object CBOR parse, clientDataJSON type/challenge byte offsets Feeds all of the above, plus the on-chain signature payload
PackedUserOperation encoding + the ERC-4337 v0.7 EntryPoint hash Byte-exact or the operation is rejected

These already exist in the Dart client and are pinned there by ground-truth vectors. Porting them independently into each new client is exactly the divergence risk that vectors/reading-key.json exists to prevent for the reading key — and the same argument that made shipping that fixture inside the package worthwhile.

Housing them here, with cross-language vectors alongside the existing ones, means every port is checked against one artifact rather than trusted to reimplement four byte-exact constructions correctly.

Happy to contribute these if the direction is agreeable — in which case the packaging question above decides what the module boundaries should look like, so it's worth settling first.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions