feat(sdk)!: dual ESM+CJS build with an exports map; take Veil off the root barrel - #40
Conversation
… root barrel
The package was CommonJS-only — `main`, no `module`, no `exports` — and the root
barrel re-exported Veil, whose vendored wasm-bindgen glue is built for the
*nodejs* target: it does `require('fs').readFileSync(...)` and instantiates the
module at evaluation time. So importing one HKDF function evaluated that glue.
In a browser bundle it broke outright; where it loaded, it cost the consuming
origin a `wasm-unsafe-eval` in its CSP.
That is the wrong trade to force on precisely the origin that derives reading
keys. A reading key is long-lived and its published half is write-once, so one
injected script that reads it decrypts everything that user has ever received,
unrotatably — `script-src 'self'` on that origin is worth keeping.
- `.` and `./crypto` ship ESM + CJS; `./veil` is CJS, which is what it always
was. `src/veil` is excluded from the ESM build, so the tree is wasm-free
structurally rather than by convention.
- `@bytesbrains/maktub-sdk/crypto` is derivation + ECIES + hybrid envelope with
no ethers, no Veil, no WebAssembly. Verified: a browser bundle of it is 88.7 kB
with zero wasm references.
- `"sideEffects"` names the wasm glue as the only side-effecting module.
- `./vectors/*` and `./deployments/*` stay reachable — the exports map would
otherwise have silently broken the deep paths 0.1.0-dev.4 documented.
BREAKING CHANGE: Veil moves from the package root to
`@bytesbrains/maktub-sdk/veil`; named exports unchanged. Deep paths into `dist/`
are no longer reachable.
Refs #39
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
ⓘ Qodo reviews are paused because your trial has ended. Ask your workspace admin to add credits to resume reviews. Manage billing |
🤖 wrokin code reviewSummaryNo high-signal issues — implementation fully addresses #39’s browser-safe packaging (dual ESM/CJS, Veil off root barrel, exports gate). Real issues (bugs / security / correctness)None found. The diff correctly splits the SDK into separate trees, gates Veil behind a CJS-only subpath with appropriate Notes
Model: deepseek/deepseek-v4-pro · your key, your model (BYOK) · Context: #39 |
🤖 wrokin security auditNo security-relevant findings (injection, authz, secrets, unsafe deserialization, etc.) were identified in this pull request. The changes are structural and improve the security posture by isolating WASM-containing code from the default import path and providing a CSP-safe entry point, with appropriate verification gates. No remediation is needed. Model: deepseek/deepseek-v4-pro · your key, your model (BYOK) · Context: #39 |
🛡️ wrokin security hunterNo vulnerabilities were proven in this pull request — the hunter found nothing it could exploit and prove by execution. Advisory only — this check never blocks your PR. |
Addresses part 1 of #39 — the packaging half. Part 2 (the four smart-wallet client pieces) is untouched; see the note at the bottom.
What was wrong
The reporter's diagnosis holds, and the eager-instantiation question they flagged as unmeasured resolves the bad way:
sdk/src/veil/wasm/warden_wasm.js:356-358is the nodejs wasm-bindgen target. It doesat module-evaluation time.
veil.ts:22imports it top-level, andindex.ts:144re-exported Veil from the root barrel. Soimport { deriveReadingKeyFromPrfOutput }instantiated WebAssembly — and becausefs.readFileSyncdoes not exist in a browser, the root barrel didn't merely cost a CSP directive there, it could not load at all.The CSP argument in the issue is the right frame for why this matters more than bundle size, and I've kept it in the code comments rather than only in this PR: a reading key is long-lived and its published half is write-once, so the origin deriving it is the single worst place to give up
script-src 'self'.What this does
../cryptocrypto/only — no ethers, no WebAssembly./veiltscpasses —dist/esmanddist/cjs— sharingtsconfig.base.json.src/veilis excluded from the ESM config, so the wasm-free property is structural, not a convention someone can undo with a stray re-export.exportsmap,modulefield,sideEffectsnaming the wasm glue as the one side-effecting module.package.jsontype marker, so the root"type"stays unset and is not load-bearing. I chose that over"type": "module"at the root (which the issue suggested) purely to keep the blast radius off the repo's own tooling — consumers see the same thing either way../vectors/*and./deployments/*are explicitly mapped. Worth flagging: anexportsmap is deny-by-default, so without these the map would have silently brokenrequire("@bytesbrains/maktub-sdk/vectors/reading-key.json")— the deep path 0.1.0-dev.4's changelog told language ports to use.Verification
Not asserted — checked.
npm run verify:packagingruns as part ofnpm run build, soci.ymlandsdk-publish.ymlboth execute it with no workflow change:The WebAssembly check traps the constructors before importing, so it covers the transitive graph rather than what a grep can see. It's scoped to
./cryptodeliberately: the root entry pullsethers→ undici, and undici compiles its own llhttp wasm on Node. That's Node's HTTP stack, absent from any browser bundle, and trapping globally just reports it as our defect.Beyond CI, I packed the tarball and installed it into a scratch consumer:
And bundled
./cryptofor the browser with Vite to confirm the actual claim: 88.7 kB, zero hits forwasm/veil/WebAssembly/ethers.sdk-publish.ymlgains one gate: everyexportstarget must be present in the tarball.verify:packagingchecks the working tree and cannot seepackage.json#files, so a build correct locally could still publish a map pointing at unshipped files — an install-timeERR_PACKAGE_PATH_NOT_EXPORTEDon a version number that's already burned.Existing suite: 97 tests, 13 files, all passing; typecheck clean.
@noble/curvesand@noble/hasheswere phantom dependencies.src/crypto/imports them directly in six files, but neither was independencies— the lockfile had them as"dev": true, resolving only becauseethershoisted them into place.I added them as real dependencies because otherwise this PR ships a
./cryptosubpath that fails to resolve under pnpm's strict layout or Yarn PnP, which would undercut its whole purpose. The ranges (^1.2.0,^1.3.2) resolve to exactly the versionsethersalready pins, so nothing moved and no code changed — but it does mean the SDK now declares runtime dependencies where it previously declared none, and that's your call, not mine. It's a four-line revert inpackage.jsonif you'd rather handle it on its own.Known, not fixed
crypto/aes.tsdoesawait import("crypto")as a Node fallback for AES-GCM. Browser bundlers emit an externalization warning for it. I left it alone: the fallback is real (Node 18 predates stable globalcrypto.subtle, andenginessays>=18), it's dynamic so it never enters the bundle, and silencing the warning properly means abrowserexport condition — a design change that doesn't belong in this PR.Breaking
Veil moves off the package root. Named exports are unchanged, only the import path:
Deep paths into
dist/are also no longer reachable. Version bumped to0.1.0-dev.5with a migration note in the changelog.On part 2
The four smart-wallet pieces do belong in this repo — the contracts they derive against are here (
contracts/v3/wallet/, ERC-1167Clones.cloneDeterministic+ OZWebAuthn/P256+ 4337 v0.7),deployments/base-sepolia.jsonalready carries the factory and implementation, andvectors/is the established home for byte-exact cross-language fixtures. The SDK has no wallet surface at all today, so it'd be net-new rather than a move.Two things probably want deciding before anyone starts on it: whether the wallet layer belongs in the SDK or is app-layer under "simplicity in the protocol, complexity in the app", and — if it lands — that it should be its own
./walletsubpath, which this PR now makes cheap to add.🤖 Generated with Claude Code