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
Summary
@aztec/sqlite3mc-wasm@5.1.0wraps the vendored SQLite loader to keep it bundler-friendly, injecting alocateFilehook viaglobalThis.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.wasmat afile://URL pointing at the build machine'snode_modules, which the browser refuses to fetch: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.tssets the hook on the shared state object and then calls the loader:The vendored
sqlite3.mjsentrypoint begins by assigning that same property from its first argument:So the wrapper's hook is replaced on every call — with
undefinedwhenever the caller passes no options.Module['locateFile']then takes its fallback branch:Bundlers inline
import.meta.urlas the module's path on the build machine, which is where thefile://URL comes from. The statically analyzableSQLITE3_WASM_URLis emitted and hashed correctly by the bundler — it is simply never used.@aztec/kv-storeis such a caller:yarn-project/kv-store/src/sqlite-opfs/worker.tscallsawait 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_URLand the fallback happen to resolve to the same vendored file.Reproduction
No bundler required. With
@aztec/sqlite3mc-wasm@5.1.0installed, patchglobalThis.fetchto log and call the wrapper:Observed — the raw vendored path, not
SQLITE3_WASM_URL:Calling the vendored loader directly with the option is honoured, which isolates the mechanism:
In a bundled app the same fallback produces the build-machine
file://URL, and the fetch is blocked.Suggested fix
Pass
locateFileas an init option instead of writing it onto shared state, so the loader copies it across itself. A caller-suppliedlocateFileshould still win: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
locateFileis consulted forsqlite3.wasmwhensqlite3InitModule()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)yarn-project/sqlite3mc-wasm/src/index.ts, vendoredvendor/jswasm/sqlite3.mjs