Skip to content

@aztec/sqlite3mc-wasm: locateFile escape hatch is overwritten by the vendored loader, so bundled apps request sqlite3.wasm from a build-machine file:// path #25010

Description

@olehmisar

Disclosure: this issue was written and filed by an AI coding agent (Claude, via Claude Code) on behalf of a developer who hit this while upgrading an app to Aztec v5.1.0. The diagnosis and the reproductions below were carried out by the agent; the root cause was confirmed by running the code, not only by reading it. Please treat the analysis as a starting point and verify before acting on it.

Summary

@aztec/sqlite3mc-wasm@5.1.0 wraps the vendored SQLite loader to keep it bundler-friendly, injecting a locateFile hook via globalThis.sqlite3InitModuleState.emscriptenLocateFile. That hook is unconditionally overwritten by the vendored loader before it is ever consulted, so it has no effect.

The visible consequence is that a bundled browser app asks for sqlite3.wasm at a file:// URL pointing at the build machine's node_modules, which the browser refuses to fetch:

Fetch API cannot load
file:///<build-dir>/node_modules/.pnpm/@aztec+sqlite3mc-wasm@5.1.0/node_modules/@aztec/sqlite3mc-wasm/vendor/jswasm/sqlite3.wasm
due to access control checks

Since v5 the browser PXE store is SQLite over OPFS, so this is on the path of anything that opens a wallet in a bundled app.

Root cause

yarn-project/sqlite3mc-wasm/src/index.ts sets the hook on the shared state object and then calls the loader:

state.emscriptenLocateFile = (path, prefix) =>
  path === 'sqlite3.wasm' ? SQLITE3_WASM_URL.href : new URL(path, prefix || import.meta.url).href;
return sqlite3InitModuleUnwrapped(...args);

The vendored sqlite3.mjs entrypoint begins by assigning that same property from its first argument:

const sIM = globalThis.sqlite3InitModule = function ff(...args){
    sIMS.emscriptenLocateFile = args[0]?.locateFile ;
    sIMS.emscriptenInstantiateWasm = args[0]?.instantiateWasm ;
    return originalInit(...args)...

So the wrapper's hook is replaced on every call — with undefined whenever the caller passes no options. Module['locateFile'] then takes its fallback branch:

Module['locateFile'] = function(path, prefix) {
    if( this.emscriptenLocateFile instanceof Function ){
      return this.emscriptenLocateFile(path, prefix);
    }
    return new URL(path, import.meta.url).href;   // <-- taken
}.bind(sIMS);

Bundlers inline import.meta.url as the module's path on the build machine, which is where the file:// URL comes from. The statically analyzable SQLITE3_WASM_URL is emitted and hashed correctly by the bundler — it is simply never used.

@aztec/kv-store is such a caller: yarn-project/kv-store/src/sqlite-opfs/worker.ts calls await sqlite3InitModule() with no arguments.

Note this is not bundler-specific — the hook is dead in unbundled usage too. It only goes unnoticed there because SQLITE3_WASM_URL and the fallback happen to resolve to the same vendored file.

Reproduction

No bundler required. With @aztec/sqlite3mc-wasm@5.1.0 installed, patch globalThis.fetch to log and call the wrapper:

const realFetch = globalThis.fetch;
globalThis.fetch = (u, ...r) => { console.log('>>> FETCHED:', String(u)); return realFetch(u, ...r); };
const { default: init } = await import('@aztec/sqlite3mc-wasm');
await init();

Observed — the raw vendored path, not SQLITE3_WASM_URL:

>>> FETCHED: file:///<path>/@aztec/sqlite3mc-wasm/vendor/jswasm/sqlite3.wasm

Calling the vendored loader directly with the option is honoured, which isolates the mechanism:

const { default: rawInit } = await import('@aztec/sqlite3mc-wasm/vendor/jswasm/sqlite3.mjs');
await rawInit({ locateFile: p => (p === 'sqlite3.wasm' ? 'https://example.test/marker.wasm' : p) });
// >>> FETCHED: https://example.test/marker.wasm

In a bundled app the same fallback produces the build-machine file:// URL, and the fetch is blocked.

Suggested fix

Pass locateFile as an init option instead of writing it onto shared state, so the loader copies it across itself. A caller-supplied locateFile should still win:

const locateFile = (path: string, prefix: string) =>
  path === 'sqlite3.wasm' ? SQLITE3_WASM_URL.href : new URL(path, prefix || import.meta.url).href;

const sqlite3InitModule: typeof sqlite3InitModuleUnwrapped = (opts, ...rest) =>
  sqlite3InitModuleUnwrapped({ locateFile, ...opts }, ...rest);

Verified against a production build of a bundled app: the wasm then resolves to the emitted hashed asset and is served as application/wasm (1058719 bytes). Applied downstream as a package patch in the meantime.

A regression test asserting that locateFile is consulted for sqlite3.wasm when sqlite3InitModule() is called with no arguments would catch this — the current wrapper passes type checking and bundling, and only fails at runtime.

Environment

  • @aztec/sqlite3mc-wasm@5.1.0, reached via @aztec/kv-store@5.1.0@aztec/pxe@aztec/wallets (browser entrypoint)
  • Reproduced both under a webpack-based production build and standalone in Node 24
  • Affected source: yarn-project/sqlite3mc-wasm/src/index.ts, vendored vendor/jswasm/sqlite3.mjs

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions