Skip to content

Merge main into next: forward-port the 0.14.x line onto the 0.15 surface#177

Merged
WiktorStarczewski merged 53 commits into
nextfrom
wiktor/merge-main-into-next-2
Jun 11, 2026
Merged

Merge main into next: forward-port the 0.14.x line onto the 0.15 surface#177
WiktorStarczewski merged 53 commits into
nextfrom
wiktor/merge-main-into-next-2

Conversation

@WiktorStarczewski

Copy link
Copy Markdown
Collaborator

What

Merges main (0.14.x release line, through 0.14.11) into next (0.15 series), keeping next's 0.15 API surface while bringing over every main-only feature and fix — 43 commits. This unblocks the Miden Wallet's migration to 0.15: four wallet-critical APIs existed only on main until now.

Restored / ported onto 0.15

Feature Origin 0.15 adaptation
ClientOptions.useWorker opt-out + MidenConfig.useWorker #149 composes with next's workerMode (off ⇒ no shim at all; workerMode only picks classic/module when the shim is on)
TransactionProver.newCallbackProver(jsFn) #149 browser-gated #[wasm_bindgen] block (js_sys::Function has no napi shape; Node binding keeps local/remote)
crates/mobile-prover (C-ABI native prover) #149 deps re-pinned to the 0.15 git client; concurrent now cascades from miden-client itself. Wire format note: 0.15 changed TransactionInputs/ProvenTransaction serialization — native binaries must be rebuilt per protocol line
_withInnerWebClient + depth-tracked re-entrancy fix #152 grafted into next's _serializeWasmCall
Multi-threaded WASM proving (ST/MT dual build, /mt + /mt/lazy subpaths, nightly pin) #134 merged with next's node/napi build pipeline; mt-threads × nodejs guarded by compile_error!; MT diagnostics additionally browser-gated
inspectable dropped from JsAccountUpdate/JsStorageMapEntry/JsStorageSlot/JsVaultAsset #150 carried
react-sdk fixes: useConsume ownership, TransactionId.toHex() #138, #140 hooks adopted toHex; next-only test mocks updated to match real WASM behavior
BlockHeader.nativeAssetId() → exposed as feeFaucetId() pre-split 0.15 protocol renamed FeeParameters::native_asset_idfee_faucet_id; the wallet's fee/native-asset discovery depends on this getter, which was absent from next entirely
MidenClient.ready() / waitForIdle() / lastAuthError() pre-split next's js_export rewrite had silently dropped these; restored incl. the full SignCallbackErrorlast_sign_error capture pipeline (wallet's ensureSdkWasmReady + auth-error classification depend on the first and last)
CI: linked-client-pr gate status-cap + dead-trigger fixes (#163), stable-toolchain ST path, docs-skip changes gating, dist-tree WASM size walk various next-side twins kept where they existed; main-only deltas ported. auto-release-main.yml deleted (main-branch-only workflow)

Conflict-resolution policy

next's side for everything 0.15-shaped (workspace version 0.15.0, git-pinned miden-client + MIDEN_CLIENT_REF, napi/node build, js_export structure); main's side for the MT build system and CI hardening; unions for CHANGELOG / CLAUDE.md / knip and the exports maps (node condition + dist/st layout + /mt subpaths). Locks regenerated, never hand-merged.

Verification

  • make format-check / toml-check / typos-check / clippy-wasm
  • cargo clippy/build -p miden-mobile-prover (native, release) ✅
  • Full pnpm build (ST + MT + types + node glue) ✅; check:wasm-types, check:standalone-types, check:method-classification
  • pnpm run test — 1130/1130 ✅; react typecheck
  • check:publint + check:attw on the merged exports maps ✅; check:knip
  • Downstream proof: the Miden Wallet's 0.15 migration branch compiles fully against this branch (its 4 remaining type errors are exactly useWorker / newCallbackProver / ready / feeFaucetId, all restored here) — wallet PR will carry the Web SDK PR: marker

⚠️ Merge strategy

Please merge this with a true merge commit, not squash. The previous unification (#39) was squashed, which erased the merge parentage — that is exactly why all 43 of these commits re-conflicted this time. A merge commit makes the next main→next sync trivial.

Release

After this lands, a separate patch release PR should bump 0.15.0-alpha.40.15.0-alpha.5 so the wallet can consume the restored surface from npm. Release notes must flag: nightly toolchain now required for the MT build; new /mt subpaths; react-sdk is now esm-only (cjs consumers need dynamic import); mobile-prover wire-format break for native plugins.

* docs: import planning docs from miden-client (web SDK ones)

Per @SantiagoPittella's review on miden-client #1992: 5 of the 13
stray planning .md files at the miden-client repo root are web-SDK-
relevant — they belong here, not in the Rust client repo. Importing
under docs/planning/ with an index README that calls out their
snapshot-not-maintained status.

  AGENTS.md
  Api.Rework.Impl.md
  REACT_SDK_PLAN.md
  SimplifiedAPI.md
  SimplifiedAPI.sdk.review.md

The miden-client side (#1992 commit e20e6261a) deletes all 13 stray
docs; the other 8 are either already-tracked issues, stale, or non-
client concerns and don't need to be ported.

* docs: keep only AGENTS.md from imported planning docs

* ci: replace paths-ignore with intra-job filter so docs-only PRs aren't blocked by required checks that never run
* chore: add .nvmrc + lefthook + lint-staged for local dev hygiene

* chore: fix lefthook config — drop broken per-file tsc, use pnpm exec, guard prepare
* chore(web-client): swap chai expect for @playwright/test expect

* chore(web-client): swap puppeteer Page type for @playwright/test

* chore(web-client): drop mocha/chai/puppeteer/esm/ts-node devDeps + .mocharc.json

The legacy mocha-based Playwright wiring has been fully replaced by
@playwright/test (assertions) and vitest (unit tests). Remove the dead
test infrastructure:

- crates/web-client/.mocharc.json (deleted)
- crates/web-client/tsconfig.json: drop the "ts-node" block
- crates/web-client/package.json devDependencies removed:
  - mocha
  - chai
  - puppeteer
  - esm
  - ts-node
- pnpm-lock.yaml regenerated

Verified: pnpm --filter @miden-sdk/miden-sdk run test:unit passes
(295 tests across 13 files).
* chore(deps): bump miden-client to 0.14.5

Bumps the workspace miden-client dependency on `main` from 0.14.0 (locked
at 0.14.4) to 0.14.5, the latest 0.14.x patch on crates.io. No API
changes required on the web-sdk side — same minor line, just patch-level
updates from upstream miden-client.

The `next` branch separately tracks miden-client `next` (0.15.0
unreleased) and is unaffected by this bump.

* docs: clarify lazy entry — SSR / controlled WASM init, not 'most users skip crypto'

The previous README framing of `@miden-sdk/miden-sdk/lazy` as for 'apps
where most users never touch crypto' is misleading. The actual point of
the lazy entry is to allow usage in environments that hang on top-level
await (Next.js / SSR, Capacitor WKWebView) or where the caller wants to
explicitly control when the WASM-init cost is paid.

Also adds the missing `await MidenClient.ready()` contract: until you
call it, every wasm-bindgen type imported from the lazy entry is just a
stub that throws on construction. Async SDK methods await internally
and are exempt.

* docs: add React-side lazy guidance — gate UI on isReady from useMiden()

Apps using @miden-sdk/react with the lazy entry don't call
MidenClient.ready() directly; MidenProvider does it for them and
exposes the readiness state through useMiden() as { isReady,
isInitializing, error }. Adds the contract + a minimal example,
plus pointers to the loadingComponent / errorComponent provider
props for the zero-glue case.
* chore(ci): add publint + arethetypeswrong gates for published packages

Adds publint and @arethetypeswrong/cli as root devDeps and wires three
new scripts:
- check:publint   - runs publint per published workspace package
- check:attw      - runs attw --pack . per published workspace package
- check:publish   - builds web-client, react-sdk, vite-plugin then runs
                    both gates

Filters target the three publishable packages by name
(@miden-sdk/miden-sdk, @miden-sdk/react, @miden-sdk/vite-plugin), which
skips the private web_store workspace member that has no exports map
worth checking.

A new workflow (.github/workflows/check-publish.yml) runs check:publish
on PRs and pushes to main/next. It mirrors the WASM build setup from
test.yml's build-web-client-dist-folder job (Rust toolchain, sccache,
binaryen, Swatinem/rust-cache) and uses MIDEN_FAST_BUILD on the WASM
build since publint/attw inspect package shape and types, not the
optimization level of the WASM blob.

Mode: STRICT. Per the task rule (document only if both tools find > ~3
issues per package; otherwise leave strict), only @miden-sdk/miden-sdk
clearly clears that bar - the other two packages have <= 1 attw issue
and 1 publint warning each, so a doc-and-tolerate path would be
overkill. The CI gate will be red on first PR; the author should decide
whether to fix the underlying export map issues or merge red.

Local results today (built with MIDEN_FAST_BUILD=true):

publint (exit 0 - warnings/suggestions only):
- @miden-sdk/miden-sdk:    1 suggestion  (pkg.browser refactor)
- @miden-sdk/react:        1 warning + 2 suggestions
                           (types ambiguous under "import" condition;
                            missing "type" field; repository.url shape)
- @miden-sdk/vite-plugin:  1 warning + 2 suggestions (same shape as react)

attw (exit 1 - real failures):
- @miden-sdk/miden-sdk:    CJSResolvesToESM, InternalResolutionError
                           across multiple matrix cells, NoResolution
                           on the /lazy subpath under node10
- @miden-sdk/react:        FalseCJS (masquerading-as-CJS) under node16
                           from-ESM for both . and /lazy; NoResolution
                           on /lazy under node10
- @miden-sdk/vite-plugin:  FalseCJS under node16 from-ESM

Per task scope, no source or build-config changes were made to address
these - the gate ships first, the fixes are followup work.

* chore(miden-sdk): fix exports map for attw + publint compliance

- Split the `exports` conditions into explicit `import` blocks with
  `types` listed first, eliminating the ambiguous-types warning publint
  reported. Drop the implicit `default` fallthrough that caused
  attw to flag CJSResolvesToESM under the node16-cjs profile.
- Add an `.attw.json` selecting the `esm-only` profile. The package is
  `"type": "module"` and ships only ESM artifacts (rollup output +
  WASM glue); `require()` consumers must already use a dynamic import.
  The `esm-only` profile communicates this intent to attw and makes
  the node10 / node16-cjs columns informational only.
- Add a post-build step (`scripts/post-build.js`) that:
    1. Rewrites extensionless relative specifiers in the published
       `dist/*.d.ts` files (e.g. `from "./api-types"` ->
       `from "./api-types.js"`). Without explicit extensions, Node16
       type resolution flags an `InternalResolutionError` on every
       relative import inside `dist/index.d.ts` and `dist/api-types.d.ts`.
    2. Emits a `lazy/package.json` shim at the package root pointing at
       `dist/index.{js,d.ts}` so node10 resolution (which doesn't read
       `exports`) can still locate `@miden-sdk/miden-sdk/lazy`.
- Add `lazy` to the `files` array so the shim ships in the tarball.
- Expose `./package.json` from `exports` to keep tooling that reads
  the manifest at runtime working under strict resolution.

Public import paths (`@miden-sdk/miden-sdk` and
`@miden-sdk/miden-sdk/lazy`) are unchanged. Verified that the
existing vitest unit suite (295 tests) still passes against the new
build output.

* chore(react-sdk): fix exports map for attw + publint compliance

- Split each `exports` subpath into explicit `import` / `require`
  conditions with `types` listed first. publint flagged the previous
  shape because `types: "./dist/index.d.ts"` resolved as CJS under the
  `import` condition (FalseCJS / ambiguous-types). The new shape uses
  the `.d.mts` declaration tsup already emits for the ESM build, so
  TypeScript sees an `.mts` declaration when resolving via `import`
  and a `.d.ts` declaration when resolving via `require`.
- Add `"type": "commonjs"` to silence publint's package-type-detection
  suggestion (tsup emits `.js` as CJS and `.mjs` as ESM, matching).
- Add a `lazy/package.json` shim at the package root that points at
  `dist/lazy.{js,mjs,d.ts}`. node10 resolution doesn't read the
  `exports` map, so `@miden-sdk/react/lazy` previously failed to
  resolve under that column; the shim is the standard fix.
- Expose `./package.json` from `exports` and add `lazy` to the `files`
  array so the shim ships in the tarball.

Public import paths (`@miden-sdk/react` and `@miden-sdk/react/lazy`)
are unchanged. Verified that the existing vitest unit suite (719
tests) still passes.

* chore(vite-plugin): fix exports map for attw + publint compliance

- Split the `exports["."]` block into explicit `import` / `require`
  conditions with `types` listed first. publint flagged the previous
  shape because `types: "./dist/index.d.ts"` was interpreted as CJS
  when resolving via the `import` condition (FalseCJS / ambiguous
  types under `import`). The new shape uses the `.d.mts` declaration
  tsup already emits for the ESM build, so TypeScript sees an
  `.mts` declaration when resolving via `import` and a `.d.ts`
  declaration when resolving via `require`.
- Add `"type": "commonjs"` to silence publint's package-type-detection
  suggestion (tsup emits `.js` as CJS and `.mjs` as ESM, matching).

Public import path (`@miden-sdk/vite-plugin`) is unchanged. Verified
the existing vitest suite (27 tests) still passes.

* chore(react-sdk): drop CJS output, ship ESM-only

* chore(web-client): deduplicate lazy/package.json shim

* chore(ci): prettier-format post-build.js

* chore(react-sdk): rename CJS configs to .cjs after ESM-only flip

Switching @miden-sdk/react to "type": "module" makes .js files ESM by
default, which breaks two CJS-syntax files:
- packages/react-sdk/eslint.config.js (uses module.exports + require())
- packages/react-sdk/test/serve-tests.js (uses require())

Renamed both to .cjs. Updated playwright.config.ts:34 to reference the
new filename. ESLint auto-discovers either extension; no other consumer
references the renamed paths.
* chore: add vitest workspace + root test script

* chore(vitest): migrate from defineWorkspace to defineConfig projects (vitest 3 modern shape)

* chore(ci): exclude root vitest.config.ts from eslint typed-linting
* chore: add knip for unused-exports/deps detection

Lands knip 6.7.0 plus a baseline knip.jsonc config covering the four
TS-bearing workspaces (`packages/react-sdk`, `packages/vite-plugin`,
`crates/web-client`, `crates/idxdb-store/src`) and a root scripts entry.

Mode: warning-only. The script is `knip --no-exit-code` so CI does not
go red on day-one findings; promoting to strict mode is a follow-up
once the baseline backlog is cleared.

Baseline findings (pnpm run check:knip, exit 0 in --no-exit-code):

  Unused files          4   (3 are .d.ts ambient declarations in
                              crates/web-client/js/types/, plus
                              packages/react-sdk/src/__tests__/utils/test-utils.tsx)
  Unused dependencies   3   (root prettier; web-client @rollup/plugin-typescript, dexie)
  Unused devDeps        5   (root and idxdb-store @typescript-eslint/eslint-plugin;
                              web-client http-server + mocha;
                              react-sdk http-server)
  Unlisted dependency   1   (web-client test references @aspect-build/aspect-rsdoctor)
  Unlisted binary       1   (vite in .github/workflows/wallet-pages.yml)
  Unresolved imports    4   (web-client tests import ./eager.js / ./index.js
                              relative paths — likely dist-time URLs)
  Unused exports       23   (all in packages/react-sdk test mocks)
  Unused exported types 9   (react-sdk SignerContext + types/index.ts re-exports)
  Duplicate exports     3   (playwright.global.setup.ts; react-jsx-runtime.js;
                              vite-plugin index.ts midenVitePlugin|default)

Per-workspace tally:
  root                       1 dep, 1 devDep
  packages/react-sdk         1 file, 1 devDep, 23 exports, 9 types, 1 dup-export
  packages/vite-plugin       1 dup-export
  crates/web-client          3 files, 2 deps, 2 devDeps, 1 unlisted dep,
                             1 unlisted bin, 4 unresolved imports, 1 dup-export
  crates/idxdb-store/src     1 devDep

No CI workflow added in this commit — that lands separately once the
baseline is cleaned up. No source code modified.

* chore(web-client): drop dead @aspect-build import in sync_lock test

The 'waiters are rejected when sync times out' test destructured
acquireSyncLock/releaseSyncLock/releaseSyncLockWithError from a stale
@aspect-build/aspect-rsdoctor path that has nothing to do with this
codebase, then immediately fell back to nulls and never used the
bindings. The only thing actually exercised below is client.syncState(),
so the destructuring + bogus import was pure dead code.

Flagged by knip as the sole 'unlisted dependency' import in
crates/web-client; removing the dead block both fixes the finding and
makes the test honest about what it's checking.

* chore: drop unused devDeps + deps flagged by knip

Removes packages that no source file, build config, lint config, test, or
script references. Verified each by grepping the repo for imports / CLI
invocations / config references before deletion.

- root: @typescript-eslint/eslint-plugin (root eslint.config.js wires
  parser only; no plugin-rules block, so the plugin pkg sits idle)
- crates/idxdb-store/src: @typescript-eslint/eslint-plugin (its
  eslint.config.mjs uses 'typescript-eslint' meta-package, not the
  legacy plugin)
- packages/react-sdk: http-server (no script or workflow runs it)
- crates/web-client: http-server, mocha (rollup + playwright + vitest
  pipeline does not invoke either; no .mocharc, no http-server script)
- crates/web-client deps: @rollup/plugin-typescript (rollup.config.js
  uses node-resolve + commonjs + wasm-tool only — no TS plugin), dexie
  (only the idxdb-store crate uses dexie, and it lists its own copy)

Lockfile regenerated via pnpm install --no-frozen-lockfile.

* chore: drop unused files + dead exports flagged by knip

react-sdk:
- Delete src/__tests__/utils/test-utils.tsx — exports renderWithProvider,
  renderHookWithProvider, etc., none of which any test imports. The repo
  uses renderHook/render directly from @testing-library/react.
- Strip 'export' off internal mock helpers and types in __tests__/mocks/
  (createMockOutputNote, createMockTransactionRecord, MockNoteFilter and
  ~17 other Mock* constants, MockWebClientType, createMockSignCallback,
  createMockAccountStorageMode). They were never imported across module
  boundaries; making them module-private is correct.
- Delete the createMockSdkModule factory along with the Mock* class /
  enum constants that only existed to populate it. No test in the repo
  ever called the factory, so it pulled an entire wing of dead code.
- Remove unreachable type re-exports: GetKeyCallback / InsertKeyCallback
  / NoteId / NoteVisibility / StorageMode were re-exported through
  src/types/index.ts but src/index.ts never re-exported them, so they
  were not part of the package's public surface. The underlying types
  stay locally-typed where they're actually used.
- Make ClientWithTransactions in src/utils/transactions.ts module-private
  (the noteFilters.ts copy is the one consumers use).

web-client:
- Delete crates/web-client/.mocharc.json (only consumer was mocha, which
  was removed in the previous commit) and the matching ts-node /
  esm devDeps + the now-orphan ts-node block in tsconfig.json.

Public API of @miden-sdk/react is unchanged: every type still exported
through src/index.ts continues to be exported. Only types that were
never reachable through the package entry have been demoted.

* chore(knip): allowlist public-API exports + ambient .d.ts files

After deleting actual dead code, the remaining findings are all cases
where knip can't statically see the consumer:

- crates/web-client/js/types/{index,api-types,docs-entry}.d.ts: shipped
  as the package's published types via 'cpr js/types dist' in the build
  script, so they ARE the consumer of themselves at publish time.
  Registered as entry points with a comment naming the post-build copy
  step.
- ./eager.js, ./index.js: dynamic imports inside page.evaluate()
  callbacks, executed in the browser against http://localhost:8080
  (i.e. the dist/ output served by the test http server). Resolved at
  test runtime, not relative to the Playwright test file. Allowlisted
  via ignoreUnresolved with a comment.
- ./crates/miden_client_web: wasm-bindgen module emitted into dist/ by
  the rollup rust plugin; the .d.ts files reference it but it doesn't
  exist until after build. Same allowlist with a comment.
- prettier: invoked from the repo Makefile via 'pnpm exec prettier .';
  knip doesn't scan Makefile rules. Allowlisted in ignoreDependencies.
- vite: invoked from .github/workflows/wallet-pages.yml via 'pnpm exec
  vite build' inside the wallet example workspace, which has its own
  package.json + lockfile not visible to this monorepo's graph.
  Allowlisted in ignoreBinaries.
- Three intentional dual-export sites (vite plugin named+default, the
  Playwright test fixture's named+default 'test', and the React JSX
  runtime shim's jsx/jsxs/jsxDEV aliases): rules.duplicates set to
  'off' globally with a comment enumerating each case so a future
  change knows when to revisit it.

Also dropped the redundant entry patterns and the empty docs/** ignore
patterns that knip flagged as configuration hints.

After this, 'pnpm exec knip' exits 0 with zero findings; the 3 remaining
'configuration hints' are about react-sdk's package.json exports
pointing at not-yet-built dist/lazy.* files, which is correct and
doesn't affect the exit code.

* chore(knip): flip check:knip to strict mode

All baseline findings have been triaged in the preceding commits, so the
warning-only flag is no longer needed. Knip now fails CI on any new
unused export / dep / file / unresolved import.

* chore(ci): wire knip into lint workflow

* chore(ci): restore dexie + prettier-format knip files

Knip's static scan flagged dexie as unused, but it's bundled into the
web-client test page at runtime via rollup — page.evaluate blocks load
the bundle from localhost:8080 which transitively imports dexie. The
removal broke 22 integration tests with "Failed to resolve module
specifier 'dexie'". Re-added dexie@^4.0.1 to crates/web-client deps
and registered it in knip's top-level ignoreDependencies with a comment.

Also runs prettier --write on knip.jsonc and two react-sdk source
files that were missed in the earlier cleanup pass.

* chore(knip): allowlist publint/attw + .cjs serve-tests rename
Removes overlapping formatting rules (semi, comma-dangle, eol-last,
space-before-blocks, keyword-spacing, no-multiple-empty-lines) from the
root eslint.config.js so Prettier 3.x is the single source of truth for
style. Logic rules (camelcase, @typescript-eslint/no-unused-vars) are
preserved.

Adds eslint-config-prettier as the last entry in both flat configs
(eslint.config.js and packages/react-sdk/eslint.config.js) to disable
any remaining stylistic rules pulled in transitively from rule presets,
preventing eslint and prettier from fighting.

Extends .prettierignore to mirror the eslint configs' ignore list (dist,
target, node_modules, generated .d.ts, docs, idxdb-store codegen) so
prettier --check stays scoped to source we actually own. The existing
.prettierrc.json (trailingComma: es5) is left as-is.

Verified: pnpm --filter @miden-sdk/react run lint shows 0 errors (same
5 pre-existing warnings as main); prettier --check on packages/react-sdk
and packages/vite-plugin source passes with no reformatting needed.
…#35)

The pre-split miden-client release workflow shipped all three packages on
a tagged release; the web-sdk port dropped react-sdk and vite-plugin
publishing while keeping web-client. End users `npm install
@miden-sdk/react` were therefore stuck on the `next` dist-tag (set by
publish-web-client-next.yml) instead of getting the latest tagged release.

This restores parity with pre-split behaviour:

- New scripts/check-react-sdk-version-release.sh and
  scripts/check-vite-plugin-version-release.sh — same pattern as the
  existing check-web-client-version-release.sh (compare package.json
  version against the tagged commit's parent, set should_publish flag).

- publish-web-client-release.yml now runs each package's version-bump
  check and conditionally builds + publishes that package. The three
  publishes are independent, so a release that bumps only one package
  republishes only that one.

- The web-client build runs whenever EITHER web-client OR react-sdk needs
  to ship, since react-sdk's build consumes the WASM dist.
Match the secret name to the cargo env var (and to miden-node's
convention) so the publish-crates workflow picks up the org-managed
secret without an indirection.
…ale AGENTS (#38)

- Add a top-level CLAUDE.md aimed at AI agents (and humans skimming
  for build/lint/test/release conventions). Captures pnpm-only rule,
  the Makefile-driven workflows, runExclusive, the eager/lazy entry
  contract, the npm-registry-driven publish gate, and cross-repo
  coordination notes. The README already links to it (line 337).
- Refresh packages/react-sdk/CLAUDE.md against the current source:
    * useNotes returns notes/consumableNotes (not input/consumable)
    * useAccounts returns accounts/wallets/faucets (no 'all')
    * Mutation hooks expose action-named callbacks (send, mint, ...)
      and a 'result' field — not generic { mutate, data }
    * useSend / useMultiSend take assetId / recipients (not faucetId
      / outputs)
    * SendResult has txId (not transactionId)
    * SignerAccountConfig uses publicKeyCommitment + accountType
    * Hook reference table reorganized into query/mutation buckets
      and includes the previously-undocumented hooks (useNoteStream,
      useSyncControl, useTransactionHistory, useImportNote / Export*,
      etc.)
- Remove docs/planning/AGENTS.md — 7 lines of miden-client leftover
  that referenced 'yarn prettier' (the repo migrated to pnpm). All
  the still-useful content is now in the top-level CLAUDE.md.
* ci(changelog): add changelog gate + filtered web-sdk CHANGELOG.md

Mirror miden-client's changelog gate. Each PR must touch one of the
three changelog files; trivial PRs override with the 'no changelog'
label.

Files:
- CHANGELOG.md (new) — pre-filled with miden-client's [web]-tagged
  entries up through 0.14.x. Source-of-truth changelog for the WASM
  client (@miden-sdk/miden-sdk).
- packages/vite-plugin/CHANGELOG.md (new, stub) — placeholder so the
  CI script has a real path to diff against.
- packages/react-sdk/CHANGELOG.md (already exists) — unchanged here.
- scripts/check-changelog.sh (new) — mirrors miden-client's script,
  extended with a third path for the vite-plugin.
- .github/workflows/changelog.yml (new) — fires on PR open / sync /
  label changes.

* ci(changelog): collapse to single root CHANGELOG.md + auto-populate release notes

- Drop packages/react-sdk/CHANGELOG.md and the packages/vite-plugin/
  CHANGELOG.md stub. The single root CHANGELOG.md is the source of
  truth for every published artifact in this repo.
- Simplify scripts/check-changelog.sh accordingly: only the root file
  is checked. The 'no changelog' label still overrides.
- Add scripts/extract-changelog-section.sh: takes a version (with or
  without a leading 'v'), prints the matching '## <version> (...)'
  section body to stdout. Used by the release-notes workflow.
- Add .github/workflows/release-notes.yml: on release:published, looks
  up the matching CHANGELOG section and replaces the release body via
  'gh release edit --notes'. Logs a warning + leaves the body untouched
  if no matching section is found (no failure — release shouldn't be
  blocked on a stale changelog; the gate at PR time is where that's
  enforced).
* ci: migrate heavy jobs to WarpBuild for ~3x speedup

* chore: ignore docs/superpowers

* chore: remove tracked docs/superpowers (now gitignored)
#46)

* fix(ci): wait for node-builder/prover gRPC port instead of sleep+pgrep

Root-cause fix for the recurring 'Integration tests (ci-shard-N)'
flake that intermittently surfaces as 'TypeError: Failed to fetch' from
the in-browser gRPC client during ensure_genesis_in_place.

The previous startup pattern was:
  ./bin/testing-node-builder &
  sleep 4
  pgrep -f testing-node-builder || exit 1
which only checked process liveness, not that the gRPC listener had
bound to its TCP port. On slower CI runners, the test job would race
the listener and fail the very first RPC call (get_block_header_by_number)
with a JS-level fetch failure.

Replaced with a /dev/tcp probe loop with a 30s timeout. Same fix
applied to all three startup sites:
- integration-tests-web-client: testing-node-builder on :57291
- integration-tests-remote-prover-web-client: testing-node-builder on :57291
- integration-tests-remote-prover-web-client: testing-remote-prover on :50051

If the port isn't reachable in 30s the step still fails with a clear
error (and prints whether the process is running at all).

* fix(ci): wait for node-builder/prover stdout readiness signal, not bare TCP

Previous fix (b02c1dc) replaced sleep+pgrep with a TCP-probe loop on
:57291. The probe consistently passes within 6s on WarpBuild runners,
but tests STILL hit 'TypeError: Failed to fetch' from the in-browser
gRPC client.

Root cause: the node-builder binds its public TCP listener early but
doesn't finish wiring up the gRPC services (store, ntx-builder, block-
producer, validator) until `NodeBuilder::start().await` returns.
A bare TCP probe to :57291 succeeds before the gRPC service is
registered — in-browser fetches race in and fail.

The reliable readiness signal is the explicit
'Node started successfully with PID' stdout line printed AFTER
start().await returns (see miden-client
crates/testing/node-builder/src/main.rs:55).

This commit:
- Pipes node-builder stdout to /tmp/node-builder.log and waits
  (timeout 60s) for the readiness line to appear before returning
  from the step.
- Same treatment for the remote-prover step ('Remote prover
  listening on <addr>' from prover/src/main.rs:87).
- On timeout, dumps the last 50 lines of the captured log so the
  failure mode is visible.

* fix(ci): force line-buffered stdout via stdbuf -oL on test backends

Addendum to 931e36a. The stdout-grep readiness check on the captured
log was timing out because Rust's stdout is BLOCK-buffered when
connected to a file (`>/tmp/node-builder.log`) instead of
line-buffered when connected to a TTY. The 'Created data directory'
line happened to flush, but 'Node started successfully' (printed
~80 bytes later) never reached the buffer fill threshold (~8KB) so
it stayed in memory and the grep loop timed out at 60s.

Wrap both backend launches with `stdbuf -oL` (coreutils) to force
line-buffered stdout, which makes println output visible to the
log file as soon as the newline is written. Same fix on the remote
prover step.

* fix(ci): revert readiness probe over-engineering, add Playwright retries: 1

Backing out 931e36a + a31c792. The root-cause hypothesis (gRPC
listener bound before service registered) led to a stdout-grep
readiness check that consistently TIMED OUT — start().await either
hangs or stdout doesn't flush properly when redirected to a file
(stdbuf -oL didn't fix it). All 4 integration shards failed solid
on the over-engineered version.

Restore the original `sleep 4 && pgrep` startup pattern (which was
flaky at maybe 1-3% of runs but functional 97% of the time), and
add `retries: 1` to playwright.config.ts on CI to mask the residual
flake without doubling test wall-clock.

The actual root cause of the original 'TypeError: Failed to fetch'
flake remains unidentified — not enough signal in failed runs to
isolate it. If it becomes a real problem (>5% of runs), revisit
with debug instrumentation in the test harness or the node-builder
binary itself rather than the workflow YAML.
* ci(publish): consolidate to single workflow + trusted publishing (main)

Replaces the two-workflow setup (publish-web-client-release.yml +
publish-web-client-next.yml) with a single publish-web-sdk.yml that
handles both the GitHub-release event AND the next-channel patch-release
PR-merge event in one file.

Why one file: npm's trusted publishing has a (package, repo) uniqueness
constraint — only ONE workflow filename per package can be the trusted
publisher. Probed empirically: registering trust for a second workflow
on the same package returns 409 Conflict, including with a different
`--env` scope. To use trusted publishing for both flows, both must
come from the same workflow file.

Migration to trusted publishing:
- permissions.id-token: write at workflow + publish-job level → GitHub
  mints the OIDC token npm exchanges for a short-lived publish credential.
- node-version bumped to 24 + force-update of npm CLI to ensure ≥ 11.5
  (required for OIDC support).
- NPM_WEBCLIENT_TOKEN env vars dropped from every publish step.
- --provenance flag added to every `npm publish` / `pnpm publish`
  for the signed attestation that ships with the package.

Branching by event:
- release:published → `--tag latest`, gated by check-*-version-release.sh.
- pull_request:closed (against next, with patch-release label) → `--tag
  next`, gated by check-*-version-pr.sh. Inert on main (branch filter
  excludes main-targeted PRs); kept here for parity so the same trust
  config works across both long-lived branches.
- vite-plugin still ships only on release events (no pr.sh script for
  it). Tracked as future work.

Trust registration follow-up: each of the 6 published packages has its
trust pointing at `publish-web-client-release.yml`. After this lands,
huna1 (npm org owner) revokes those + re-registers against
`publish-web-sdk.yml` for both branches' workflow location. Until that
happens the publish job will fail at the publish step (no trust →
401).

* fix(ci/publish): guard skipped job + pin npm to ^11.5

Two review-feedback fixes on the consolidated publish workflow:

1. `skipped:` job no longer fires misleadingly when `check-version`
   itself was skipped. Previously: a PR merged to `next` without the
   `patch release` label rejected at `check-version`'s `if:`, all
   `should_publish_*` outputs were empty strings, the `!= 'true'`
   checks all evaluated true, and `skipped:` ran printing
   "All publish gates failed" — even though no gating ever happened.
   Adding `needs.check-version.result == 'success'` to the guard
   prevents that.

2. Pin the global npm install to `^11.5` instead of `@latest`. A
   future npm 12 with breaking changes to `--provenance` / OIDC
   semantics would silently break this workflow under @latest. The
   ^11.5 floor still gets the OIDC support we need.
* ci: auto-cut GitHub release on 'patch release' merge to main

* docs: changelog entry for auto-release workflow
…ain) (#53)

* fix: restore exec bit on check-react-sdk-version-pr.sh

* fix(ci): wait for node-builder readiness via pty (real fix)

Spawn testing-node-builder + testing-remote-prover via 'script -qfec'
(allocates a pty), then poll the captured log for the binary's own
'Node started successfully' / 'Remote prover listening' stdout signal.

Why each prior approach failed:
- 'sleep 4 && pgrep': only proves the process is alive, not that the
  port is bound or gRPC handlers are registered. ~10% flake.
- TCP probe (/dev/tcp): kernel-level handshake succeeds the moment
  TcpListener::bind() is called, but tonic's gRPC service dispatcher
  registers AFTER that. Racy.
- 'stdbuf -oL': only modifies libc stdio buffering. Rust's println!()
  goes through std::io::stdout()'s own BufWriter (~8KB block buffer
  on non-tty fds) and ignores stdbuf entirely. Captured log stayed
  empty until the buffer filled, long past the readiness window.

The pty is the actual fix: Rust detects the tty fd and constructs
stdout as a LineWriter, so println!() flushes on every newline.
'script -qfec' is preinstalled (bsdmainutils) on Ubuntu runners.

Centralized into scripts/wait-for-bg-binary.sh so the same probe
applies at all 4 spawn sites (3 node-builder + 1 remote-prover).

* Revert "fix(ci): wait for node-builder readiness via pty (real fix)"

This reverts commit 0ad7700.
…ENT_REF v0.14.4 → v0.14.5 (#59)

* fix(ci): introduce wait-for-grpc.sh with strict 3-digit HTTP code check (sibling of next)

Sibling fix to next-side. PR #57 (the original main-side flake fix)
shipped a probe that false-positived on shard-4: reported 'gRPC
dispatch responsive after 1 attempt (HTTP 000000)' and tests then
ran against a not-yet-ready server, hitting the original
TypeError: Failed to fetch.

Two issues:
1. The contrived gRPC-web payload `--data-binary "$(printf '\x00\x00...')"`
   tripped bash's null-byte stripping in command substitution. The
   payload was always empty after substitution; the printf was useless.
2. The success check `[ "$http_code" != "000" ]` was too loose. On
   the failing run curl somehow emitted "000000" (a six-digit string,
   probably from an internal retry concatenation), which 'is not 000'
   and the loop broke immediately.

Fix: drop the fancy POST, use a plain GET (any HTTP response from
tonic — even 404/405/415 — proves the dispatcher is up). Tighten
the success regex to require an exact 3-digit [1-5][0-9][0-9] code.
Failures and timeouts continue to produce '000' which now correctly
fails the regex and loops.

Note: PR #57 was never merged. This PR brings the script forward
plus the test.yml wiring (the 3 spawn site replacements that were
in PR #57).

* ci(test): wire wait-for-grpc.sh into all 3 spawn sites

* ci: bump MIDEN_CLIENT_REF v0.14.4 -> v0.14.5

The HTTP-probe readiness check (introduced earlier in this PR) works
on next branch where MIDEN_CLIENT_REF=dab6cf7b... but breaks main on
v0.14.4 — every test fails with TypeError: Failed to fetch even
though the probe gets HTTP 200. The older testing-node-builder
appears to misbehave after handling a non-gRPC HTTP request.

Bump to v0.14.5 to pick up the corresponding stable client release;
the probe should be compatible with this slightly newer binary.
Required by npm's provenance verifier. Without it, publishes via
trusted publishing fail with:

    422 Unprocessable Entity
    Failed to validate repository information: package.json:
    "repository.url" is "", expected to match "https://github.com/0xMiden/web-sdk"

react-sdk and vite-plugin already had the field; only web-client
was missing. Discovered when 0.15.0-alpha.3 published the platform
packages but rejected the web-client publish on next.
Observed flake: probe returns HTTP 200 once on the first attempt that
clears the connection-refused phase, exits, tests start, ALL tests fail
with 'TypeError: Failed to fetch' to the gRPC backend. The single-probe
gate isn't strict enough — a one-shot 200 (e.g. tonic-health responding
before the rest of the dispatcher is fully wired) currently passes.

Upgrade the readiness signal to N consecutive HTTP successes spaced
PROBE_INTERVAL apart (defaults: 3 successes, 0.5s apart), so the probe
only declares the server ready after ~1s of demonstrably-stable
response. Any non-success in the streak resets it to zero and the
slow-poll loop resumes — so a momentary blip during init doesn't get
counted twice on either side.

Tracked occurrences across recent PR runs: web-sdk PR #23 ci-shard-4,
PR #29 ci-shard-1 + ci-shard-4, PR #27 multiple shards.
* ci: auto-patch miden-client dep from PR description marker

When a web-sdk PR description contains:

    Client PR: #1234
    Client PR: 0xMiden/rust-sdk#1234   (cross-repo / fork form)

a new composite action (.github/actions/inject-linked-client-pr) parses
the marker, resolves the linked PR's head ref, and rewrites web-sdk's
miden-client (and miden-client-sqlite-store) dep in place to point at
that branch — only on the runner, never committed. The build then runs
against the unreleased upstream code, while the committed Cargo.toml
diff stays clean. Replaces the manual '[patch]' / branch-edit dance
that this session has been carrying on every migration PR.

Why in-place rewrite instead of [patch.crates-io] / [patch."<url>"]?
Web-sdk's 'next' pins miden-client at git+url=...miden-client.git+
branch=next; pointing the same URL at a different branch via [patch]
errors with 'patches must point to different sources'. In-place
rewrite covers both 'main' (crates.io dep) and 'next' (git dep)
uniformly — the rewritten line is wrapped in marker comments that
preserve the original verbatim so a cleanup step can restore it.

Components:

  .github/actions/inject-linked-client-pr/action.yml
    Composite action: parse marker, resolve head, validate state
    (closed-without-merge fails the run loudly), patch Cargo.toml +
    refresh Cargo.lock, post a sticky PR comment summarizing the
    patch. Strict 0-or-1 comment invariant: only fires when called
    with comment=true (one designated job per workflow run), and
    deletes the prior comment if the marker is later removed.

  .github/workflows/build.yml + test.yml
    Wired the action into the cargo-compiling jobs (build-wasm,
    build-web-client-dist-folder, verify-release-build). build-wasm
    is the single comment-poster (comment=true).

  .github/workflows/check-linked-client-pr.yml
    Mergeability gate: keeps a 'linked-client-pr-ready' check on the
    PR. Stays pending while the linked client PR isn't merged-and-
    reachable from the target branch's canonical refs (miden-client
    next for next-targeted PRs, latest miden-client release tag for
    main-targeted). Re-evaluates every 15 min so the check goes green
    automatically once upstream catches up — no need to re-push.
    Configure branch protection to require it.

  scripts/dev-with-client-pr.sh
    Local-dev mirror: applies the same in-place dep rewrite to your
    working tree. Idempotent. '--clear' restores the originals
    byte-for-byte from the marker block.

  lefthook.yml
    Pre-commit guard: refuses any commit while the marker block is
    present in Cargo.toml. So a forgotten 'apply' can't accidentally
    leak into a commit.

  CLAUDE.md
    Documents the marker convention, the local script, and the
    mergeability gate.

* fix(ci): use POSIX [[:space:]]* instead of \s in marker grep

GNU grep on the runner balked with 'repetition-operator operand invalid'
on the \s* in the regex. \s is a Perl-extension class that grep -E
doesn't recognize in POSIX-extended mode; the * after it ends up
applying to nothing, hence the error. Replace with [[:space:]]* across
the composite action, the dev script, and the readiness gate workflow.

* fix(ci): anchor Client PR marker regex to start-of-line

Without the ^[[:space:]]* prefix, doc examples or table cells in the
PR body that mention 'Client PR:' inline (e.g. inside backticks for a
test-plan checklist) are picked up as if they were real markers — the
action then tries to fetch a fake miden-client PR and fails with
'Failed to fetch'. Anchoring requires the marker to start its own
line, so inline mentions in surrounding prose stay decorative. Doc
examples that use placeholders like #<NUM> instead of #N (a literal
number) are also rejected by the existing #[0-9]+ requirement.

Pairs with the PR-body cleanup that replaces the literal example
'Client PR: #1234' with 'Client PR: #<NUM>' so it's clearly a template.
gh's REST API returns pull-request state as 'open' / 'closed', GraphQL
returns 'OPEN' / 'CLOSED'. The closed-PR guardrail in the composite
action and the dev script were comparing against the uppercase form,
so the closed-without-merge branch always fired even on open PRs —
the action exited with 'is closed without merge' on every run with a
'Client PR:' marker.

Normalize the API-returned value via tr to lowercase, then compare to
'open'. Verified against miden-client#2059 (state='open' from REST).
…ment (#69)

The auto-patch action's parse + Cargo.toml-rewrite steps succeed, but
the sticky-comment step fails with 'Resource not accessible by
integration' because build.yml only declares contents: read at the
workflow level. The error short-circuits the composite action and the
build job fails before cargo even runs.

Two-part fix:

  build.yml -> grant pull-requests: write to the build-wasm job only.
    Scoped to that job (rather than at the workflow level) so every
    other job keeps the read-only default. Principle of least privilege:
    only the comment-poster needs the write.

  inject-linked-client-pr/action.yml -> mark the sticky-comment steps
    (both upsert and delete) as continue-on-error: true. The comment is
    decorative — if write permissions ever go missing again, or if the
    sticky-comment marketplace action has a transient blip, we shouldn't
    abort the rewrite step's gains. The patch is already on disk at the
    point those steps run.
PR #25's verification run surfaced a coverage gap: my initial wiring
in #65 only patched build.yml's build-wasm and test.yml's
build-web-client-dist-folder + verify-release-build. But Clippy WASM
(in lint.yml) and publint + attw (in check-publish.yml) ALSO compile
cargo against the workspace's miden-client dep — without the patch,
they hit canonical crates.io `miden-client = "0.14.5"` and fail
with `error[E0599]: no variant named `ApplyTransactionAfterSubmitFailed``
when the PR depends on an unreleased upstream variant.

Add the action call after checkout in both jobs. Both run with
comment=false (the default); build-wasm remains the single sticky-
comment poster.
…#73)

Two issues from PR #25's verification run:

  1. The 'linked-client-pr-ready' custom commit status the gate posts
     never landed. Cause: the workflow's permissions block has
     'checks: write' (for check-runs) but not 'statuses: write' (which
     the POST /repos/.../statuses/$sha endpoint requires). The gh api
     POST 403'd silently — and #2 below ate the error.

  2. set_status() redirected gh's stdout to /dev/null. With no error
     visible, the silent 403 looked like 'job ran successfully' even
     though the readiness verdict never reached the commit.

This commit:
  - adds 'statuses: write' to the workflow permissions
  - drops the >/dev/null redirect so any future API failure is loud
#75)

Previous form:
  read -r body base_ref state <<<"$(gh api ... --jq '"\(.body // "")\t\(.base.ref)\t\(.state)"')"

PR bodies are multi-line and the marker is itself space-separated. read
defaults to whitespace IFS and only consumes through the first newline,
so a body starting with 'Client PR: #2059\n...' assigned:
  body  = 'Client'
  base_ref = 'PR:'
  state = '#2059'

The gate then hit the early-return:
  if [ "$state" != "open" ] && [ "$state" != "OPEN" ]; then
    echo "PR #$PR_NUM is $state — skipping."
    exit 0
  fi

…and exited with the visible-in-the-job 'PR #25 is #2059 — skipping.',
never posting the linked-client-pr-ready custom status. The job logged
'success' because exit 0 is what the early-return does, but the
readiness verdict never reached the commit.

Fix:
  - body = separate gh api call (multi-line tolerated naturally)
  - base_ref + state packed into a tab-separated single-line, read with
    IFS=$'\t' so a hypothetical future whitespace in either field
    wouldn't repeat the bug
  - Same IFS=$'\t' added to the second read of (merged, merge_commit_sha)
    for consistency, even though neither field can contain whitespace
    today
…arker (#77)

The README's Contributing section was a stub. Move it into a proper
CONTRIBUTING.md (GitHub auto-detects this filename and surfaces it on
new-issue / new-PR pages) and expand it with the cross-repo workflow:

  - The 'Client PR: #N' marker convention (and the cross-repo /
    fork form 'Client PR: 0xMiden/miden-client#N')
  - What CI does with the marker (auto-patch action, sticky PR
    comment, readiness gate)
  - Branch-protection guidance (require linked-client-pr-ready, NOT
    the gate matrix job)
  - Local-dev parity (scripts/dev-with-client-pr.sh)
  - The three situations where you still want to hand-edit
    Cargo.toml instead of using the marker

README's Contributing section is now a one-line pointer.
…DK API changes (#127)

* docs(CLAUDE.md): document required surfaces for MidenClient + React-SDK API changes

Adds a 'Documenting public-API changes' section to the repo CLAUDE.md
that maps every doc surface a contributor has to touch when changing the
public API of either the MidenClient resource layer (web-client) or the
React SDK. Replaces the vague 'update relevant per-package CLAUDE.md'
bullet in the Contributing checklist with a precise mapping table.

Surfaces enumerated for each side: TS types (api-types.d.ts), resource
JSDoc, README narrative, per-package CLAUDE.md, root CHANGELOG, and the
typedoc curation file when applicable. Includes triggers (when to touch
each), conventions (terse tone, no speculative docs, cross-link PRs,
one source of truth per fact), and a doc-only-PR escape hatch.

Triggered by web-sdk#31, where the new transactions.batch API needed
docs in five different places — easy to miss without an explicit map.

* docs(CLAUDE.md): add the published-docs-site surfaces to the doc-process map

The first pass missed three load-bearing surfaces of the public-docs
pipeline:

1. The canonical user-facing site at https://docs.miden.xyz, hosted from
   0xMiden/miden-docs, ingesting upstream repos via deploy-docs.yml's
   docs/external/src/* → docs/builder/<repo>/ copy step.

2. The typedoc-generated API reference at docs/typedoc/web-client/, which
   is regenerated from docs-entry.d.ts and verified by CI via
   git diff --exit-code (drift fails the build).

3. docs/external/src/ as the canonical narrative source the deploy-docs
   workflow ingests. After the web/WASM split this directory needs to
   exist in this repo (currently a gap — miden-client's docs/external/src
   no longer carries web/React content), and pages under it need to be
   added when shipping new public capabilities.

Adds a 'Where the docs are published' table at the top of the doc-process
section, plus a typedoc-regen workflow snippet, plus README ⇄ Docusaurus
parity rule. Expands both the MidenClient and React SDK surface tables
to call out docs/external/src/ and docs/typedoc/web-client/ explicitly,
and updates the contributing-checklist item 3 to enumerate them.

* fix(clippy): drop redundant & in export.rs format! arg

Newer nightly clippy fires `useless_borrows_in_formatting` on the
pre-existing &account_id.to_string() pattern. The lint started firing
between origin/main's last green CI run (2026-04-30) and today —
nightly clippy rolls forward unpinned in this repo's Makefile.

Same fix as web-sdk#31's commit 7f9cf07. Including it here so the
CLAUDE.md doc-process change isn't blocked by an unrelated baseline
lint regression on the target branch.

* fix(fmt): collapse format! to single line per nightly fmt

After dropping the redundant & in commit 2b27e16, the call fits on one
line and cargo +nightly fmt --check fails until we collapse the
multi-line layout. Same pattern as the next-targeted PR (which already
had it on one line).
The first pass of the doc-process section (#126/#127) prescribed
'regenerate typedoc and commit the diff' for any change to the curated
public surface. That guidance was wrong: the in-repo
'Check that web client documentation is up-to-date' CI step runs
typedoc and then a git diff --exit-code over the regenerated tree, but
the directory hasn't been tracked since the web/WASM split — so the
diff is empty and the step is a warning-only smoke test, not a gate.
typedoc output is build artifact, not source.

Updates four spots in the doc-process section:
- 'Typedoc — keep in sync with the public API surface' →
  'Typedoc — regenerated by CI, don't commit'
- The MidenClient surface table row for docs/typedoc/web-client/ now
  reads 'Don't commit. Regenerated by CI...'
- The Conventions bullet 'Regenerate typedoc' became 'Don't commit
  typedoc'
- Contributing checklist item 3 mirrors that

Triggered by web-sdk#31 audit feedback: noticed the autogen tree
shouldn't have been committed there either. Companion .gitignore
addition lands separately on web-sdk#31.
WiktorStarczewski and others added 17 commits May 11, 2026 20:23
…134)

* feat(web-client): multi-threaded WASM proving via wasm-bindgen-rayon

Enables real parallel STARK proving in the browser by wiring wasm-bindgen-rayon
through the SDK's WASM worker. Empirical: mint 22.9s -> 11.8s, consume 11.2s ->
5.1s, send 11.2s -> 6.5s on testnet (10 hardware threads, ECDSA accounts, fast
build profile). All proofs verified by the testnet node and committed to chain.

Toolchain + linker (.cargo/config.toml, rust-toolchain.toml):
  - nightly required for cfg(target_feature = "atomics") and -Z build-std
  - target-feature=+atomics,+bulk-memory,+mutable-globals applied at the
    [target.wasm32-unknown-unknown] level so it reaches every crate (including
    wasm-bindgen-rayon's compile_error guard, which RUSTFLAGS-via-env shadowed)
  - linker flags --shared-memory --import-memory so the WASM module imports
    a shared memory the host can clone into spawned workers; --max-memory=4GiB
  - explicit --export of __wasm_init_tls / __tls_size / __tls_align /
    __tls_base, otherwise lld GC's them and wasm-bindgen-cli's threading-prep
    pass fails ("failed to find __wasm_init_tls")

Cargo (crates/web-client/Cargo.toml):
  - direct dep on miden-crypto with feature "concurrent" so cargo's feature
    unification turns on real rayon (dep:rayon) AND p3-maybe-rayon/parallel
    for the indirect deps that miden-client -> miden-tx -> miden-crypto
    pulls in. miden-crypto/concurrent is std-clean (unlike miden-tx/concurrent
    which transitively requires tempfile/tokio that don't compile to wasm32)
  - direct dep on rayon for diagnostic functions (rayonThreadCount,
    parallel/sequential SumBench)
  - wasm-bindgen-rayon = "1.3"

Build pipeline (crates/web-client/rollup.config.js, package.json):
  - -Z build-std=std,panic_abort added to baseCargoArgs so std is recompiled
    with atomics enabled; without this, target-feature=+atomics emits a wasm
    with the bit set but cfg(target_feature = "atomics") still returns false
    in std code, breaking wasm-bindgen-rayon
  - target-feature flags moved out of --config build.rustflags (which
    RUSTFLAGS env shadows) and into .cargo/config.toml's
    [target.wasm32-unknown-unknown].rustflags (uniform application)
  - new wasmBindgenRayonSnippetResolver: rollup plugin (order: pre) that maps
    wasm-bindgen-rayon's workerHelpers.js import('../../..') to the wasm-bindgen
    output's index.js (rollup-plugin-rust internal resolveId would otherwise
    point '../../..' at a directory and fail EISDIR)
  - new emitWorkerHelpers factory: writes a sibling workerHelpers.js next to
    Cargo-*.js in dist/ and dist/workers/ with the start_worker shim
    (postMessage protocol from wasm-bindgen-rayon, but using __wbg_init named
    export instead of pkg.default since rollup-plugin-rust doesn't synthesize
    a default in bundler mode)
  - [remove-wasm-tla] plugin's "already exported?" guard was a false-positive
    on `default: __wbg_init,` inside the wasm-bindgen frozen-namespace object,
    so the export-rewrite was being skipped. Detect the actual top-level
    `export { ... }` token list instead

JS init (crates/web-client/js/{constants.js, index.js, workers/web-client-methods-worker.js}):
  - rayon's global thread pool is per-WASM-instance. The SDK's web-client-
    methods-worker has its OWN WASM instance separate from the main thread,
    and every transaction's prove call runs there. initThreadPool(n) on the
    main thread alone sees rayon::current_num_threads() == 1 inside the
    worker, so par_iter()/par_chunks() in miden-crypto + p3-maybe-rayon
    fall through to sequential code despite the parallel features being on.
    Fix: pass numThreads in the SDK worker's INIT message, call
    wasm.initThreadPool(numThreads) inside the worker before the WebClient
    is constructed. numThreads defaults to navigator.hardwareConcurrency
    when crossOriginIsolated, else 1.

Rust source (crates/web-client/src/lib.rs):
  - pub use wasm_bindgen_rayon::init_thread_pool (re-exported as
    initThreadPool) for callers that want to manage pool init themselves
  - rayonThreadCount() / parallelSumBench(n) / sequentialSumBench(n)
    diagnostic exports for verifying the pool is real (synthetic 4.4x
    speedup on FP-mix workload, matching real prove's 1.7-2.1x given
    parallel-fraction + Amdahl)

Caveats and known issues:
  - This is the fast-build dev profile (MIDEN_FAST_BUILD=true,
    MIDEN_WEB_DEV=true). Release-build numbers will be different.
  - Page hosting the SDK MUST be cross-origin isolated (COOP: same-origin,
    COEP: require-corp) for SharedArrayBuffer. crossOriginIsolated === true
    is the gate; if false, the worker takes the numThreads=1 path and proves
    sequentially.
  - Diagnostic exports (rayonThreadCount, parallelSumBench, sequentialSumBench)
    should likely be feature-gated before this lands on main.

* feat(client): MidenClient._getInnerWebClient escape hatch

Public-but-@internal accessor returning the proxied JS WebClient that
backs MidenClient. The proxy forwards missing properties to the
wasm-bindgen WebClient, so callers can reach lower-level methods like
executeTransaction, proveTransaction[WithProver], submitProvenTransaction,
applyTransaction, newSendTransactionRequest, newConsumeTransactionRequest.

Intended for advanced consumers that need to split the bundled
execute → prove → submit → apply pipeline across contexts. The
0xMiden/wallet branch wiktor/mt-wasm-offscreen uses it to run prove in
a chrome.offscreen document (where wasm-bindgen-rayon can spawn a thread
pool) while keeping execute + submit + apply in the wallet's MV3 service
worker.

Marked @internal. The shape of #inner is intentionally not part of the
documented public API and may change between SDK versions. Consumers
depending on this should pin the SDK version. If a use case becomes
common enough to warrant a stable public API, file an issue.

* docs(build): record SIMD-target-feature attempt + regression

Tried `target-feature=+simd128` on top of the LTO+wasm-opt release build,
plus `--enable-simd` for binaryen. Measured a 16-37% prove-time
REGRESSION across mint / consume / send on M-series Mac in V8/Chrome:

  | op      | non-SIMD | +simd128 | delta |
  |---------|----------|----------|-------|
  | mint    | 6.3 s    | 8.6 s    | +37%  |
  | consume | 4.5 s    | 5.2 s    | +16%  |
  | send    | 3.5 s    | 4.3 s    | +23%  |

Root cause: Plonky3's Goldilocks backend has hand-tuned SIMD only for
native x86_64 (AVX2 / AVX-512 IFMA) and aarch64 (NEON). For
wasm32-unknown-unknown it falls back to scalar code, and LLVM's
auto-vectorizer for `+simd128` on u64 modular arithmetic produces v128
sequences that lose to scalar on this hardware:
  - WASM v128 has no widening multiply (i64 × i64 → i128), so
    Goldilocks reduction emulates the high-half via shift+add chains
    that cost more than the scalar `mul` they replace.
  - The branchy modular-reduction step doesn't auto-vec cleanly.
  - LLVM tags too many short loops as "vectorize me" when setup cost
    outweighs throughput.
  - V8's WASM-SIMD-to-NEON lowering on M-series isn't as polished as
    its scalar lowering for this workload shape.

Reverted at the config level. Documented the negative result in
`.cargo/config.toml` so the next person who looks doesn't repeat the
experiment without the context. Re-enabling `+simd128` should be paired
with explicit `std::arch::wasm32` intrinsics in Plonky3's Goldilocks
backend (upstream Plonky3 contribution), not just a target-feature flag.

Also dropped `--enable-simd` from wasm-opt args (no longer needed
without v128 in the input). `--enable-threads` stays (still required
for the shared-memory atomics that wasm-bindgen-rayon depends on).

Reference numbers confirmed unchanged:
  bench mint    6.3 s
  bench consume 4.5 s
  bench send    3.5 s

WASM artifact size unchanged: 15.8 MB on-disk / 4.07 MB gzipped.

* docs(changelog): add 0.14.4 entries for mt-wasm proving + _getInnerWebClient escape hatch

* fix(ci): toml format, clippy cast_precision_loss in bench helpers, build-wasm target uses nightly + build-std

- taplo fmt: rustflags table in .cargo/config.toml needed one-arg-per-line
  format; same for Cargo.toml's [package.metadata.docs.rs] entry.
- clippy: add #[allow(clippy::cast_precision_loss)] to parallelSumBench /
  sequentialSumBench. These are diagnostic helpers exercising rayon
  dispatch with a synthetic FP-mix workload — precision loss is
  intentional, the work just needs to defeat constant-folding.
- Makefile build-wasm: stable cargo trips at link with .cargo/config.toml's
  +atomics target-feature because rust-std-wasm32 from rustup is built
  WITHOUT atomics. Switch to nightly + -Z build-std=std,panic_abort to
  match what the rollup build (baseCargoArgs in rollup.config.js) already
  uses. Removes the silent divergence where CI's build-wasm was producing
  a different artifact than the published one.

* feat(build): dual-build ST + MT WASM, /mt subpaths for opt-in multi-threading

The previous mt-wasm work shipped a single multi-threaded WASM that
required cross-origin isolation just to LOAD — even consumers using
delegated proving exclusively. That was a breaking regression for
non-COI contexts. This restores the v0.14.2 default behavior (single-
threaded WASM that loads anywhere) while keeping multi-threaded as an
opt-in.

Subpaths:
  '@miden-sdk/miden-sdk'         → eager-st (default; safe everywhere)
  '@miden-sdk/miden-sdk/lazy'    → lazy-st
  '@miden-sdk/miden-sdk/mt'      → eager-mt (opt-in; requires COI)
  '@miden-sdk/miden-sdk/mt/lazy' → lazy-mt

Mechanics:
- New 'mt-threads' cargo feature on miden-client-web gates the rayon +
  wasm-bindgen-rayon deps and miden-crypto/concurrent (which propagates
  to Plonky3 via cargo feature unification, enabling the parallel
  prover paths).
- '+atomics', shared-memory + import-memory linker flags, TLS exports,
  and the -Z build-std=std,panic_abort flag all moved out of
  .cargo/config.toml into per-invocation cargo args in the rollup config.
  ST build uses stable cargo with the precompiled rust-std-wasm32 (no
  atomics, no build-std, no nightly). MT build uses nightly with all
  the previous flags.
- Rollup config is now driven by MIDEN_BUILD_VARIANT={st,mt}, output to
  dist/{st,mt}/. package.json's 'build' runs both sequentially.
  MIDEN_FAST_BUILD=true (PR CI) skips the MT step via a small
  conditional script, keeping PR CI ~15 min on the ST-only path.
- Makefile gains 'build-wasm-mt' alongside 'build-wasm' (now ST only).

Verified locally: both variants produce working WASM artifacts. ST glue
contains no 'shared:true' references; MT glue does. Total wall-clock
~5 min for both builds with MIDEN_FAST_BUILD=true on warm caches.

* fix(build): keep -Z build-std for ST too, rewrite worker wasm.js import per variant

CI failures from the dual-build push:
1. Build Client for Wasm / Build Web Client / publint+attw all hit
   'can't find crate for std'. Cause: my removal of -Z build-std=std,panic_abort
   from the ST cargo args meant ST needed precompiled rust-std-wasm32, but
   CI doesn't install that for the auto-installed nightly toolchain (it
   installs rust-src, which build-std uses). Fix: keep -Z build-std on the
   ST path too. Costs ~30s per ST build but avoids a CI workflow change.
   Stable + precompiled std for ST is a follow-up if we ever need it.
2. Worker bundle couldn't resolve '../../dist/wasm.js' — the static path
   was correct pre-dual-build, but post-change the file is at
   dist/{st,mt}/wasm.js. Added a small rollup resolveId plugin
   (rewriteWorkerWasmImport) that rewrites the import to point at the
   variant-specific output. Applied to both classic + module worker configs.
3. taplo fmt run on Cargo.toml (single-line array reflow).
4. Knip: clean.js shows as unused because my new build-types.js exec's
   it indirectly. Added clean.js to web-client entry list. Cpr added to
   ignoreDependencies (also exec'd by build-types.js).

Removed RUSTUP_TOOLCHAIN=stable overrides from the ST package.json
script and Makefile target — same root cause (CI doesn't install stable +
wasm32 target), and forcing nightly via rust-toolchain.toml works fine
for both variants now.

* feat(react-sdk): mt + mt/lazy subpaths; document COI requirement; drop cpr

React SDK (packages/react-sdk):
- Two new exports parallel to the existing /lazy split: '/mt' (eager + MT)
  and '/mt/lazy' (lazy + MT). Same source tree; tsup writes four bundles
  with file-level rewrites of '@miden-sdk/miden-sdk/lazy' to the matching
  SDK subpath ('', '/lazy', '/mt', '/mt/lazy'). Node-10 fallback shims
  added at packages/react-sdk/mt/{,lazy/}package.json mirroring the
  existing lazy/ shim.

Web SDK README (crates/web-client/README.md):
- 'Entry Points' section reworked to explain the eager × ST/MT 4-cell
  matrix. Added 'Setting cross-origin isolation headers' subsection with
  per-host snippets (Vite, Next.js, Express, Chrome/Firefox MV3
  manifest). Documented the COEP side-effect (cross-origin resources
  need CORP/CORS) and the COI service-worker shim as an option for
  consumers who can't set headers. Documented 'initThreadPool(n)' as a
  required one-time call before any MT prove.

React SDK README (packages/react-sdk/README.md):
- New 'Subpaths: Eager / Lazy × ST / MT' section replacing the old
  Next.js/SSR-only intro. Added 'Multi-threaded proving (/mt, /mt/lazy)'
  subsection with the bring-up-the-pool example using a small
  ThreadPoolBoot component inside MidenProvider.

Tooling:
- Dropped 'cpr' devDependency from crates/web-client/package.json. The
  one consumer (scripts/build-types.js) now uses fs.cpSync directly,
  which works regardless of how the script is invoked (PATH-independent).
- Knip ignoreDependencies entry for cpr removed too.

* chore(deps): refresh pnpm-lock.yaml after dropping cpr from web-client

* fix(build): scripts + typedoc + playwright now point at dist/st/ post-dual-build

After the dual-build restructure, dist/ has no top-level files —
everything's under dist/{st,mt}/. Several CI-only paths still hardcoded
the old layout:

- crates/web-client/typedoc.json: entry was './dist/docs-entry.d.ts',
  now './dist/st/docs-entry.d.ts'.
- crates/web-client/scripts/check-{bindgen-types,method-classification,
  standalone-types}.js: hardcoded dist/crates/miden_client_web.d.ts
  paths, now dist/st/crates/...
- crates/web-client/playwright.config.ts: webServer was 'npx http-server
  ./dist', now serves './dist/st' so integration tests' implicit
  page.evaluate(() => import('./index.js')) resolves correctly.

Type declarations are identical between the ST and MT variants (the
WASM impl is feature-gated but the .d.ts is uniform), so checking just
the ST output is sufficient for these scripts. Integration tests run
against ST too — the MT variant needs cross-origin isolation headers
on the dev server, separate change if/when we want to exercise it in CI.

* fix(build): always build both ST + MT variants (publint requires advertised dist files to exist)

The MIDEN_FAST_BUILD-skips-MT pattern saved ~3-5 min in PR CI but broke
publint, which reads package.json's exports field and verifies every
advertised subpath target file is in the packed tarball. With MT skipped:

  pkg.exports["./mt"].import.types is ./dist/mt/index.d.ts but the
  file does not exist.

Three options considered:
  a) Drop /mt + /mt/lazy from exports (defeats the dual-build purpose).
  b) Force MT build only inside check-publish (runs publint outside
     fast mode, ~5 min slower for that one job).
  c) Always build both (~3-5 min slower across every PR-CI job that
     runs the SDK build).

Going with (c): the invariant 'pnpm run build always produces every
variant the package.json claims' is the simplest mental model. PR CI's
~5 min increase is acceptable; the integration test shards (each
12-18 min) dominate runtime anyway.

Removes scripts/build-mt-conditional.js since its sole purpose was to
honor the now-defunct skip path.

* fix(docs): tsconfig.docs.json files now point at dist/st/ post-dual-build

Pairs with the earlier typedoc.json + check-bindgen-types.js + check-
standalone-types.js + check-method-classification.js fixes — last leftover
hardcoded dist/ path. Type declarations are identical between ST and MT
variants, so checking only ST is correct.

* fix(build): patch out rollup-plugin-rust optimize_for_size std flag

@wasm-tool/rollup-plugin-rust 3.1.x unconditionally appends
`-Z build-std-features=optimize_for_size` to the cargo invocation
when building in release+rustc-optimize mode. That causes std to be
recompiled with size-priority paths (simpler sort, smaller memcpy,
weaker HashMap hash) — the exact opposite of what a STARK prover
needs, since hot std paths in miden-crypto / p3-maybe-rayon dominate
the prove time.

Empirical regression: ECDSA-MT send goes from ~3.5s (without the
flag) to ~12s (with it). ST is similarly degraded. Affects every
release-profile build the plugin produces; PR-CI runs with
MIDEN_FAST_BUILD=true so they skip wasm-opt + LTO and don't trip
this, but the published artifact would.

Fix via pnpm.patchedDependencies — patch-package equivalent for
pnpm. Auto-applied on every install. No-op on clean upstream
installs after the line is removed in a future plugin release.

Repro: a separate Next.js bench (https://github.com/WiktorStarczewski/miden-prover-bench)
exercises 10× send→consume cycles and reports avg/median/min/max
prove times per phase. Patch confirmed by direct A/B.

* perf(mt): enable +simd128 with LLVM auto-vectorization disabled

The earlier blanket attempt at `+simd128` regressed prove time 16-37%
(commit 05dcac9). Root cause was LLVM auto-vectorization of Goldilocks
u64 modular reduction: WASM v128 has no widening 64×64 multiply, so
LLVM emitted shift+add chains that cost more than the scalar `mul` they
replaced.

Re-enable `+simd128` paired with:

  -C llvm-args=-vectorize-loops=false
  -C llvm-args=-vectorize-slp=false

This suppresses LLVM auto-vectorization globally while still letting
hand-written WASM-SIMD paths in libraries activate via
`cfg(target_feature = "simd128")` (BLAKE3 etc). Goldilocks scalar code
stays scalar.

Also adds `--enable-simd` to wasm-opt args so binaryen accepts v128
instructions in the input module.

Measured on testnet, ECDSA, M-series Mac, 10-cycle send/consume bench
in proving-bench-nextjs:

  median send:    4173 ms -> 3895 ms  (-6.7%)
  median consume: 4132 ms -> 3903 ms  (-5.5%)
  min send:       3795 ms -> 3325 ms  (-12%)
  min consume:    3799 ms -> 3499 ms  (-7.9%)

WASM size: 17.4 MB -> 16.2 MB (about 7% smaller — autovec was inflating
binary size with mis-vectorized v128 codegen).

Variance widened slightly (max-cycle outliers got worse), but typical-
case prove time is faster and best case is meaningfully faster. Net
positive for end users.

The deeper win remains upstream: hand-written std::arch::wasm32
intrinsics for Goldilocks mul/reduce in p3-goldilocks (Plonky3). That
PR would close the WASM-vs-native gap meaningfully — this flag combo
is a free incremental on top once it lands.

* review: bump to 0.14.6, pin nightly, replace _getInnerWebClient with _withInnerWebClient

Addresses review feedback on PR #134:

1. Bump @miden-sdk/miden-sdk and @miden-sdk/react to 0.14.6.
   The branch added meaningful surface (MT-threaded WASM build, dual ST/MT
   subpaths, optimize_for_size patch, simd128 + autovec disabled, the
   inner-client escape hatch); the next published artifact should reflect
   that with a version bump.

2. Pin rust-toolchain.toml to nightly-2026-05-05 (was floating `nightly`).
   Floating nightly silently rebuilds against whatever's current on each
   build, which (a) shifts the published WASM bytes between releases and
   (b) breaks PGO compatibility (profile data is keyed to LLVM version).
   Bump deliberately, never via floating channel.

3. Replace synchronous getter `_getInnerWebClient()` with callback-based
   `_withInnerWebClient(fn)` that runs `fn` inside `_serializeWasmCall`.
   The previous getter returned the proxied client and let the caller
   bypass the SDK's WASM-call serialization chain — concurrent access
   would trip wasm-bindgen's "recursive use of an object detected" RefCell
   panic. The new shape holds the lock for the duration of `fn` so
   downstream consumers don't have to bring their own mutex (the wallet
   already does, but external consumers shouldn't have to).

4. Verified `--features mt-threads` actually enables the parallel chain.
   `cargo tree -e features` is misleading here (only shows direct feature
   dependencies, hides feature unification). `cargo build -v` is ground
   truth: with mt-threads, rustc compiles for wasm32-unknown-unknown with
   --cfg 'feature="concurrent"' on miden-crypto AND
   --cfg 'feature="parallel"' --cfg 'feature="rayon"' on p3-maybe-rayon
   AND --cfg 'feature="parallel"' on p3-util and p3-miden-lifted-stark.
   Full chain unifies. (No code change for this one — verification only.)

CHANGELOG entry promoted from 0.14.4 (TBA) → 0.14.6 (TBA) and rewritten
to document the perf + build changes (simd128, optimize_for_size patch,
toolchain pin) alongside the original MT and escape-hatch features.

* docs(changelog): drop build/perf entries — internal toolchain changes don't belong in user-facing changelog

* fix(rollup): typos check rejects 'mis-vectorizing' — reword to 'incorrectly vectorizing'

* fix(react-sdk): bump peer + wallet example dep to ^0.14.6 to match web-client

* build(rollup): keep ST path on stable Rust, MT stays nightly

Decouples the two build paths' toolchain requirements. The MT path needs
nightly for cfg(target_feature = "atomics") to flip true (stable 1.93
silently emits +atomics but the cfg() check still says false, breaking
wasm-bindgen-rayon's compile-time gate) and for -Z build-std to recompile
std with atomics enabled. Nothing in the ST dep graph requires nightly
once mt-threads gates wasm-bindgen-rayon / rayon / concurrent out, so
flipping ST to stable widens trust window for downstream consumers
pinning their own toolchains and avoids breakage when nightly regresses.

Changes:
- crates/web-client/rollup.config.js: set RUSTUP_TOOLCHAIN=stable for the
  ST variant only (rustup precedence: env > rust-toolchain.toml). Move
  -Z build-std=std,panic_abort from baseCargoArgs into mtOnlyCargoArgs.
- CI workflows: install stable wasm32-unknown-unknown std alongside the
  pinned nightly so the ST build can link without -Z build-std.

Validated locally: rollup ST build produces a 14.6MB wasm in 4m58s on
stable, no errors. MT path unchanged.

Addresses review feedback on #134 from @SantiagoPittella.

* mt-threads: propagate `concurrent` through miden-client

Per Santiago's review thread (PR #134 r3196048839): the previous
`miden-crypto/concurrent` only enabled rayon parallelism in
miden-crypto (Merkle/hash primitives). The prover-side parallelism in
miden-tx + miden-prover stayed single-threaded, so the published MT
WASM bundle was shipping with only a partial multi-thread win.

Switch to `miden-client/concurrent`, which cascades:
  miden-client/concurrent -> miden-tx/concurrent -> miden-prover/concurrent
                                                 -> miden-crypto/concurrent (transitively)

Verified with:
  cargo tree -p miden-client-web --features mt-threads \
    --target wasm32-unknown-unknown -f "{p} {f}"

Before: only miden-crypto had `concurrent`.
After: miden-tx, miden-prover, and miden-crypto all have `concurrent`.

The miden-client `concurrent` feature lands in
0xMiden/miden-client PR #2169. Until that publishes to crates.io, the
workspace pulls miden-client via a temporary `[patch.crates-io]`
git ref pointing at the PR's branch — drop the patch and bump the
workspace miden-client version once the PR ships.

* mt-threads: drop committed [patch.crates-io] (use Client PR marker instead)

The previous commit erroneously committed a `[patch.crates-io]` block pinning
miden-client to the PR branch — this repo has an established convention for
exactly that scenario (web-sdk CLAUDE.md "Linking a web-sdk PR to an in-flight
miden-client PR"). Setting `Client PR: 0xMiden/rust-sdk#2169` in the PR
description makes the inject-linked-client-pr CI action apply the same patch
runner-locally without polluting the committed tree. The pre-commit
no-linked-client-pr-block hook caught my version mid-flight here; thanks
hook.

* address review: gate diagnostics behind testing; tidy comments

* chore: bump workspace + npm packages to 0.14.8; consume published miden-client 0.14.8

* fix(ci): pin linked client PR by rev (sha) instead of branch, survive post-merge auto-delete

* chore: bump example wallet SDK deps to 0.14.8

---------

Co-authored-by: Wiktor Starczewski <wiktor.s@miden.team>
…ker opt-out (#149)

* feat(rust): miden-mobile-prover crate with C ABI for iOS/Android FFI

* feat(web-client): add ClientOptions.useWorker to opt out of Web Worker shim

The WebClient shim wraps every WASM call in a Web Worker for UI
responsiveness, but the worker boundary serializes the prover via
TransactionProver.serialize() — a format with no encoding for
newCallbackProver(jsFn). A CallbackProver is silently downgraded to
'local' and the worker spawns its own in-process WASM prover,
bypassing the caller's JS callback.

This blocks mobile/Tauri/Capacitor consumers that want to route prove
through a native plugin (e.g. miden-mobile-prover on iOS/Android).

Add useWorker?: boolean to ClientOptions (default true; no behavior
change for existing consumers). When false, the WebClient skips the
worker and dispatches WASM calls directly on the current thread, so
the prover handle reaches wasmWebClient.proveTransactionWithProver
with the closure intact.

Wired through:
- crates/web-client/js/index.js: WebClient constructor + both factories
- crates/web-client/js/client.js: MidenClient.create plumbing
- crates/web-client/js/types/api-types.d.ts: ClientOptions type
- packages/react-sdk: MidenConfig.useWorker → MidenProvider's
  WebClient.createClient[WithExternalKeystore]

* fixup! feat(web-client): add ClientOptions.useWorker to opt out of Web Worker shim

* feat(web-client): add TransactionProver.newCallbackProver(jsFn)

Companion to the useWorker opt-out: provides the JS-callable prover
variant that consumers can pass to client.transactions.{send,consume,submit}
to route prove() through a JS function. The variant wraps the JS Function
in a Send+Sync impl of TransactionProverTrait whose prove() dispatches
to the closure and awaits its Promise<Uint8Array>.

Required for mobile native-prover plug-in flows (iOS/Android Capacitor
+ miden-mobile-prover) — the wallet hands the SDK a CallbackProver whose
closure base64-encodes the inputs, calls MidenNativeProver.prove via
Capacitor, and decodes the result back to Uint8Array. Combined with
ClientOptions.useWorker:false, the closure survives the WebClient
dispatch path and reaches the wasm-bindgen binding intact.

* chore(web-client): drop debug console.log from JsCallbackTransactionProver

The 6 console_log calls inside prove() were left over from
debugging the bridge end-to-end. They fired on every local prove
and produced noise in consumer devtools without conveying anything
useful that error returns don't already cover.

The console_log extern is also removed (had no other callers).

* docs(changelog): newCallbackProver + useWorker entries under 0.14.6 (TBA)

* chore(ci+docs): publish miden-mobile-prover, fix changelog version, doc Client-PR marker

- publish-crates-release.yml now publishes the miden-mobile-prover crate
  on release (preflight dry-run, then publish), alongside idxdb-store and
  web-client.
- CHANGELOG.md: move newCallbackProver + useWorker entries from the stale
  '0.14.6 (TBA)' heading (0.14.6 shipped 2026-05-05) into a fresh
  '0.14.9 (TBA)' section. 0.14.7 and 0.14.8 are also published.
- CLAUDE.md (Linked-PR section + CHANGELOG section): spell out that the
  'Client PR: #N' marker is the load-bearing handle for cross-repo CI and
  that prose mentions don't trigger the linked-PR pipeline; spell out
  that CHANGELOG entries must go under a version strictly higher than
  the latest published release (the '(TBA)' heading at the top of the
  file lags releases and isn't trustworthy on its own).

* chore(release): bump to 0.14.9

- Cargo workspace 0.14.8 → 0.14.9 (cascades to miden-client-web,
  miden-idxdb-store, miden-mobile-prover via workspace inheritance).
- npm packages 0.14.8 → 0.14.9: @miden-sdk/miden-sdk,
  @miden-sdk/react, @miden-sdk/vite-plugin.
- @miden-sdk/react's peer dep on @miden-sdk/miden-sdk bumped to ^0.14.9.
- Cargo.lock refreshed accordingly.

* chore(lint): fix format + clippy doc_markdown + toml-fmt on provers/mobile-prover

* chore(release): bump wallet example deps to 0.14.9
…nt trigger (#163)

* fix(ci): stop linked-client-pr gate from spamming failures via status cap

The "Linked client PR ready" gate re-runs every 15 min on a cron and
POSTed a fresh `linked-client-pr-ready` commit status on every run, even
when the verdict was unchanged. GitHub caps each (commit SHA, context)
pair at 1000 statuses, so any PR whose head SHA sat unpushed eventually
hit the cap; the POST then returned 422 and the job failed, emailing the
PR author every 15 minutes.

Two changes to set_status:

1. Idempotency guard: read the latest existing `linked-client-pr-ready`
   status for the head SHA and skip the POST when state + description are
   unchanged. This stops the email spam and stops accumulating statuses.

2. Tolerate the 422 "maximum number of statuses" response as non-fatal
   (the verdict is unchanged and re-POSTing is impossible anyway) for
   SHAs that already exhausted the cap, while still surfacing any other
   failure such as a 403 from a missing `statuses: write` scope.

Also routes the no-marker success path through set_status so it benefits
from the same guards; target_url is now attached only when a linked PR
is known.

* fix(ci): run linked-client-pr gate on pull_request_target events

The workflow triggers on `pull_request_target`, but the `gate` job's
`if:` guarded on `github.event_name == 'pull_request'` — a value that
never occurs under this trigger. The PR-triggered path was therefore
dead: the gate only ever ran via the schedule cron, never when a PR was
opened, edited, or synchronized. That defeats the reason the workflow
uses `pull_request_target` (so it can post a status on fork PRs).

Guard on `pull_request_target` instead so the gate evaluates and posts
`linked-client-pr-ready` on PR events as intended. The matrix's
non-schedule branch already reads `github.event.pull_request.number`,
which is populated on `pull_request_target`, so it needs no change.
…152)

* fix(web): _withInnerWebClient re-entrancy — depth-tracked inline run

* chore(release): bump web-sdk to 0.14.10

* chore(release): bump wallet example deps to 0.14.10

* review(152): add re-entrancy test to ci-shard-4 so CI picks it up

@SantiagoPittella's review noted the new
`with_inner_web_client_reentrancy.test.ts` wasn't being executed by
CI. Confirmed: the file is at the right path and the default chromium
project's `testMatch: "*.test.ts"` matches it locally, but CI runs
via the manually-balanced `ci-shard-N` projects, which only execute
files explicitly listed in their `testMatch` arrays.

Added the file to shard 4 ("compile-and-misc"), which is the natural
home for an infrastructure/API regression test (alongside the
similarly protocol-flavoured `miden_client_api.test.ts` and
`settings.test.ts`).

* fix(test): drive reentrancy test through MidenClient wrapper

The re-entrancy regression test called `client._withInnerWebClient(...)`
on `window.client`, but the test harness sets `window.client` to the
proxy-wrapped inner WebClient (created via `WasmWebClient.createClient`),
which owns `_serializeWasmCall` but not `_withInnerWebClient` —
`_withInnerWebClient` is a method on the `MidenClient` wrapper. Every
test failed with `client._withInnerWebClient is not a function`.

Wrap the existing inner client in a real `MidenClient` (exported from
index.js, attached to `window` by the harness) and drive
`_withInnerWebClient` through it. External `syncState` calls move to the
shared inner client so they still queue on the same `_serializeWasmCall`
chain the inner slot holds — the wrapper exposes `sync()`, not
`syncState()`. Touches only this test; the other shard-4 tests keep
using `window.client` as the raw inner client.

* test: rewrite reentrancy test 3 as a characterization test

Test 3 asserted that an external SDK call made WHILE `fn` is in flight
queues behind the `_withInnerWebClient` outer slot. The fix's design
makes that impossible on the same client: `_withInnerLockDepth` is a
global counter on the inner client, so while `fn` awaits (depth > 0) any
`_serializeWasmCall` — including one from code outside `fn` — runs
inline, not queued. The PR's own SAFETY CONTRACT documents this and
requires callers to hold an external mutex to avoid it. The test was
asserting a guarantee the design intentionally does not make.

Rewrite it to pin the documented behavior instead: an external call
during `fn` runs inline, so "external-ran" precedes "inner-end". Use a
cheap proxy-fallback read (local store lookup) rather than a networked
`syncState` for the external call so the ordering is deterministic and
not subject to node latency. Drop the now-unused `innerFinished`
deferred. If a future change closes the hole (context-scoped depth) or
reintroduces the deadlock, this assertion flips and flags the change.

* test: make reentrancy test 3 deterministic via explicit handshakes

The previous version asserted a natural-timing ordering between the
external call and `fn`'s completion. That ordering is a race:
`_serializeWasmCall`'s inline path is `Promise.resolve().then(fn)`, so
whether an external call runs inline vs queued depends on whether
`_withInnerLockDepth > 0` at the exact microtask it fires — two CI runs
produced opposite orderings (external-ran before vs after inner-end).

Replace the timer race with explicit handshakes: `fnEntered` ensures the
external call is issued only once `fn` is mid-flight (depth = 1, forcing
the inline path), and `fn` parks on `externalDone` until the external
call returns, so its inline execution is observable before the outer
slot resolves. A 5s `Promise.race` converts the regression case (call
queues behind the parked outer slot → deadlock) into a fast, explicit
"external-blocked" failure instead of a hang. Prettier-clean.
… / JsStorageSlot / JsVaultAsset (#150)

* fix(web): drop inspectable from JsAccountUpdate / JsStorageMapEntry / JsStorageSlot / JsVaultAsset

Resolves miden-client#2183: under Next.js 16.2 dev-mode the patched
console.* path (clientFileLogger.log) runs every non-primitive
argument through safe-stable-stringify, which invokes toJSON()
automatically. These four wasm-bindgen structs had
#[wasm_bindgen(getter_with_clone, inspectable)], which caused
wasm-bindgen to auto-emit a toJSON() reading every public field via
wasm.__wbg_get_<class>_<field>(this.__wbg_ptr) — i.e. each
console.log(update) fired 11 WASM getter calls. If the underlying
pointer had been freed or another WASM call was in flight, the
resulting 'null pointer passed to rust' trap propagated out of the
user's console.log call site and crashed the caller.

Other inspectable usages in the codebase (Address, NoteFile,
CodeBuilder, miden_array) wrap inner objects as tuple structs with no
public fields, so wasm-bindgen's auto-toJSON for them is a no-op —
they were never affected and stay as-is.

Dropping the attribute makes the JS class behave like the 15 other
wasm-bindgen exports the issue references: no toJSON() method, so
JSON.stringify / safe-stable-stringify see an empty wrapper and
return {}. Field access via the named getters is unchanged — only
the auto-stringification path is muted.

CHANGELOG entry added under a new 0.14.10 (TBA) section (0.14.9
was just shipped — per the project rule against adding to released
sections).

* test(web): regression test for miden-client#2183

Locks in the contract enforced by the previous commit: the
wasm-bindgen wrapper classes for JsAccountUpdate, JsStorageMapEntry,
JsStorageSlot, and JsVaultAsset must NOT expose a toJSON() method.

Without toJSON(), safe-stable-stringify (used by Next.js 16.2 dev-mode
console.* patching) falls back to {} on these wrappers and the
__wbg_get_<class>_<field> re-entry path can never fire.

The test runs in the existing crates/web-client Playwright harness;
it loads the built SDK on window and asserts both
(a) prototype.toJSON is undefined for all four classes, and
(b) JSON.stringify of a fabricated JsAccountUpdate instance returns
'{}'. If anyone re-adds inspectable to these structs (or otherwise
emits an auto-toJSON), the test fails immediately.

* test(web): broaden regression test to every wasm-bindgen wrapper

The original idxdb_store_no_tojson.test.ts only checked the four
classes named in miden-client#2183 (JsAccountUpdate,
JsStorageMapEntry, JsStorageSlot, JsVaultAsset). The actual failure
mode — a wasm-bindgen-auto-generated toJSON that reads fields via
`__wbg_get_<class>_<field>(this.__wbg_ptr)` — can happen to ANY
struct declared `#[wasm_bindgen(inspectable)]` with `pub` fields,
not just those four.

Rename to no_wasm_reentry_via_tojson.test.ts and widen the contract:
enumerate every export on `window` that looks like a wasm-bindgen
wrapper (static `__wrap`, prototype `__destroy_into_raw`), fabricate
an instance with `__wbg_ptr = 0`, and assert JSON.stringify returns
'{}'. That property holds for:

- classes with no toJSON (default JSON.stringify behavior),
- classes with wasm-bindgen's safe `toJSON() { return {}; }` shape
  (emitted for `inspectable` structs that have no `pub` fields —
  Address, NoteFile, CodeBuilder, the array wrappers, etc.),
- any class with an explicit toJSON that doesn't read WASM fields.

It fails only for the dangerous shape: toJSON reading fields through
WASM getters. Anyone who later adds `inspectable` to a struct with
`pub` fields will see this assertion fire with the offending class
name in the failure message, regardless of whether the new struct is
in idxdb-store or anywhere else.
Brings the 43 main-only commits (0.14.6–0.14.11) onto next, keeping
next's 0.15 API surface. Key features restored on next:

- useWorker opt-out (ClientOptions + MidenConfig) [#149]
- TransactionProver.newCallbackProver + JsCallbackTransactionProver [#149]
- crates/mobile-prover (Cargo deps still 0.14 — ported to the 0.15
  workspace deps in a follow-up commit on this branch) [#149]
- _withInnerWebClient + depth-tracked re-entrancy fix [#152]
- Multi-threaded WASM proving: ST/MT dual build, dist/st + dist/mt
  layout, /mt + /mt/lazy subpaths, nightly toolchain pin [#134]
- inspectable dropped from JsAccountUpdate/JsStorageMapEntry/
  JsStorageSlot/JsVaultAsset [#150]
- react-sdk fixes: useConsume ownership [#138], TransactionId.toHex [#140]
- linked-client-pr gate status-cap + dead-trigger fixes [#163]
- docs-skip 'changes' job gating in test.yml

Conflict-resolution policy: next's side for everything 0.15-shaped
(version 0.15.0-alpha.4, git-pinned miden-client, napi/node build,
js_export structure, MIDEN_CLIENT_REF pin); main's side for the MT
build system and CI hardening; unions for CHANGELOG/CLAUDE/knip and
the exports maps (node condition + dist/st layout + /mt subpaths).
auto-release-main.yml deleted (main-branch-only workflow).
Locks regenerated, not hand-merged.
- mobile-prover: depend on miden-client/concurrent (the 0.15 client
  exposes the feature, so the direct miden-tx/miden-protocol deps from
  the 0.14 workaround are gone); document that the TransactionInputs/
  ProvenTransaction wire format is per-protocol-line and 0.14-built
  native binaries do not interoperate with a 0.15 SDK
- provers: newCallbackProver + JsCallbackTransactionProver re-added as
  a browser-gated #[wasm_bindgen] impl (js_sys::Function has no napi
  representation, so the Node binding keeps local/remote provers only)
- BlockHeader.feeFaucetId(): 0.15 spelling of main's nativeAssetId()
  (FeeParameters::native_asset_id was renamed fee_faucet_id upstream);
  needed by wallet native-asset discovery
- lib.rs: compile_error! guard against mt-threads × nodejs; MT
  diagnostics additionally gated on the browser feature so the napi
  build with testing enabled keeps compiling
- react tests: txId mocks expose toHex() to match real WASM behavior
  (the hooks now use main's #140 toHex fix; next's mocks still returned
  the hex from toString, which masked the original regression);
  useSend prover-branch test asserts on proveTransaction(result, prover)
  — the 0.15 call shape — instead of main's proveTransactionWithProver
- CHANGELOG: 0.15.0 entries for the ported surface + feeFaucetId
A method-surface diff of client.js/index.js between main and the merged
tree (next rewrote whole regions of these files for the 0.15 port, so
git's hunk-level merge silently preferred next's side) surfaced three
public APIs that exist on main but were missing after the merge:

- MidenClient.ready() — WASM-init gate for /lazy consumers; the wallet's
  ensureSdkWasmReady() depends on it
- MidenClient.waitForIdle() — drain the serialized WASM call chain so a
  host can clear in-memory auth keys without racing the kernel's auth
  callback
- MidenClient.lastAuthError() — raw JsValue the sign callback last threw
  (typed reason/code convention); the wallet's auth-error classification
  (readLastAuthReason) depends on it. Full pipeline restored: the
  SignCallbackError carrier in web_keystore_callbacks.rs, the
  last_sign_error RefCell capture in web_keystore.rs, the browser-gated
  WebClient::last_auth_error getter (adapted from main's inner_keystore
  pattern to next's inner.borrow() cell), the SYNC_METHODS entry, the
  client.js delegate, and the api-types.d.ts declarations
…ght project

The test asserts on idxdb-store's Js* classes and the wasm-bindgen
WebClient copied onto window by the browser global setup — none of which
exist under the napi binding. main had no nodejs project, so the merge
made it run there for the first time.
The test predates the main/next split and guards both directions of the
entry contract: dist/st/eager.js must have WASM initialized at import
(sync constructors work immediately), and dist/st/index.js must NOT
TLA-init WASM (sync constructor throws before getWasmOrThrow resolves).
next's js_export/napi test-suite rewrite (#13) deleted it, leaving the
package's default entry (main/browser → dist/st/eager.js) untested;
the merge inherited that deletion and the knip ignoreUnresolved entry
for ./eager.js went stale — restoring the test un-stales it.
Excluded from the nodejs Playwright project (page fixture +
browser-only dynamic imports). Verified green locally against the
merged dist.
- ci: eager_entry.test.ts added to ci-shard-4's testMatch — main carried
  it in shard-4, the merge dropped the entry, and CI only invokes the
  shard projects, so the restored test was never executing in CI
- react-sdk: the variant rewrite in tsup.config.ts keyed off the
  /lazy specifier, but next's source tree imports the bare
  @miden-sdk/miden-sdk — the rewrite never matched and every variant
  (/lazy, /mt, /mt/lazy) silently bundled the eager-ST SDK. The rewrite
  now keys off the exact quoted bare specifier and THROWS when nothing
  matches, so a future specifier change cannot silently ship wrong
  variants again. Verified: all four dist bundles emit their correct
  per-variant subpath
- node: waitForIdle resolves immediately (the napi binding has no
  detached call chain to drain) and lastAuthError returns null (signing
  goes through FilesystemKeyStore — a JS sign callback can never run),
  so the cross-platform MidenClient type surface holds on Node instead
  of throwing TypeError
- docs: lastAuthError docstrings state the useWorker:false requirement
  (the worker shim's keystore lives in the worker WASM instance, so the
  main-thread accessor reads null there)
- test: with_inner_web_client_reentrancy first case comment matched
  main's serializing proxy; next's proxy binds fallback reads directly,
  so the case guards the borrow-check invariant, not chain re-entrancy
…roject

Same class as no_wasm_reentry_via_tojson: the tests came from main
(which had no nodejs Playwright project) and exercise the browser
worker-shim chain mechanics — _serializeWasmCall, the
_withInnerLockDepth counter, chain release on rejection. The napi
client serializes in Rust and has no JS call chain, so all four cases
TypeError'd under nodejs. They were masked in the previous run because
no_wasm_reentry aborted the suite earlier.
@WiktorStarczewski WiktorStarczewski merged commit f59884c into next Jun 11, 2026
34 checks passed
@WiktorStarczewski WiktorStarczewski deleted the wiktor/merge-main-into-next-2 branch June 11, 2026 17:10
WiktorStarczewski added a commit that referenced this pull request Jun 11, 2026
Bumps @miden-sdk/miden-sdk, @miden-sdk/react (+ peer), and the three
node-sdk platform packages to 0.15.0-alpha.5. First alpha published
from the unified branch (PR #177): restores useWorker, newCallbackProver,
_withInnerWebClient, MidenClient.ready/waitForIdle/lastAuthError,
BlockHeader.feeFaucetId, the miden-mobile-prover crate, and MT proving
via the /mt + /mt/lazy subpaths on the 0.15 protocol surface.

Release notes must flag: nightly toolchain required for the MT build;
react-sdk is now esm-only; mobile-prover wire format requires a matching
0.15 SDK (0.14-built native binaries do not interoperate).
WiktorStarczewski added a commit that referenced this pull request Jun 12, 2026
…t merge (#184)

The #177 unification dropped the _serializeWasmCall chain from the
direct (no-worker) path everywhere it existed on main:

- createClientProxy bound fallback methods (getAccount, getAccounts,
  getTransactions, ...) raw to the WASM client instead of routing them
  through the chain (SYNC_METHODS stay raw-bound by design);
- the five transaction methods and three sync methods called the raw
  client directly in their !this.worker branches.

Every such call holds the WASM client's internal RefCell across its
awaits, so two overlapping calls panic with 'RefCell already borrowed'
and poison the instance — later calls throw 'Unreachable code should
not be executed' and in-flight promises can stay pending forever.
useWorker:false consumers (Capacitor/WKWebView, callback-prover hosts)
hit this deterministically; miden-wallet's mobile build panicked at
wallet creation when front-end balance polling overlapped the initial
syncs (the mobile leg of #180).

All direct-path calls now route through the chain exactly as on main
and 0.14.x. The sync methods keep their coalescing lock as the outer
lock (sync lock → chain, in that order, on every path — no deadlock).
Covered by a burst-concurrency regression test that hangs/panics on
the unfixed build and completes in ~3s on the fixed one.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants