refactor(wallet-cli): enforce RPC param validation at dispatch time via superstruct - #8846
Merged
Merged
Conversation
sirtimid
force-pushed
the
sirtimid/wallet-cli-rpc-handler-structs
branch
from
July 1, 2026 11:31
17d10e8 to
c591c6e
Compare
sirtimid
changed the base branch from
rekm/wallet-cli
to
sirtimid/wallet-cli-daemon-e2e
July 1, 2026 11:31
sirtimid
force-pushed
the
sirtimid/wallet-cli-daemon-e2e
branch
2 times, most recently
from
July 1, 2026 14:02
9b0489c to
56d3cf9
Compare
sirtimid
force-pushed
the
sirtimid/wallet-cli-rpc-handler-structs
branch
from
July 2, 2026 09:56
c591c6e to
6cf1fde
Compare
sirtimid
marked this pull request as ready for review
July 2, 2026 10:12
Add an end-to-end suite that spawns the built `mm` CLI as real child processes and drives the full daemon lifecycle over the Unix socket — the gap left by the in-process suites (`socket-integration.test.ts` exercises the transport in-realm; `wallet-factory.e2e.test.ts` constructs a `Wallet` in-process). Covers: `start` (and the already-running guard on a second `start`), `call` returning the SRP-derived account, `status`, `stop`, persistence across a restart (the resume path: the wallet comes back locked rather than re-importing the SRP), `purge`, and the owner-only socket (0600) / data dir (0700). Because it needs the built `dist/` and the native better-sqlite3 addon, it runs as its own jest project (`jest.config.e2e.js`) via a new `test:e2e` script and is excluded from the fast unit `test` run and its 100%-coverage gate. A dedicated `test-wallet-cli-e2e` CI job (Node 20.x and 24.x) builds the dependency subtree and runs it; the per-package `test-*` matrix can't host it because it runs against source with no build. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Rename `fd` to `file descriptor` throughout the daemon spawn code and tests
- Narrow `stopDaemon` stale-cleanup to `absent`/`refused` sockets only: a
`permission`/`timeout`/`protocol` socket may be a wedged or foreign-user
daemon, so it is no longer deleted or reported as a successful stop
- Add a compile-time exhaustiveness guard to `ensureDaemon`'s ping handling so
spawning is reachable only for a positive `absent` result
- Replace the two mutable `{ value: T | null }` boxes with a single
`StartupOutcome` discriminated union
- Simplify/de-duplicate daemon comments; use `as unknown as ChildProcess` in
the spawn mocks; add tests for the new branches
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Generalize the e2e jest config to discriminate by folder instead of the feature-specific `daemon-e2e` filename suffix, so future subprocess e2e tests just drop into `tests/` with no config change. - jest.config.e2e.js: match `roots: ['<rootDir>/tests']` instead of `**/*.daemon-e2e.test.ts` - jest.config.js: ignore `<rootDir>/tests/`; drop the redundant daemon-e2e coverage exclusion (coverage is collected from `./src/**` only) - Rename lifecycle.daemon-e2e.test.ts -> lifecycle.e2e.test.ts - README: describe the suite by folder The in-process `wallet-factory.e2e.test.ts` stays in `src/` (unit suite): it needs the Web-Crypto polyfill env and is coverage-visible, unlike the subprocess suite. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ispatch Adds a `defineHandler(paramsStruct, run)` helper plus a generic `RpcHandler<TParams, TResult>` / `RpcHandlerDefinition` type so each daemon RPC method owns its params struct. `rpc-socket-server` now validates params via `superstruct.validate` once per request and returns `-32602 invalidParams` on shape mismatch, so handler bodies can trust their input. Rewrites `getStatus` and `call` against the new shape and replaces the `wallet.messenger.call as any` cast with a single labelled `RpcDispatcher` bind. Tests updated; coverage stays at 100%. Closes #8777. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Remove duplicate `test-wallet-cli-e2e` CI job (stale v2 entry merged in), duplicate `testPathIgnorePatterns` key in jest.config.js, and stale eslint-suppressions entry removed by lint:fix. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Guard activeDispose() in shutdown closure with try-catch + log; a throwing dispose during SIGTERM/SIGINT previously skipped PID/socket cleanup silently - Change callParamsStruct type annotation from Json[] to unknown[] for the tail elements, matching what the runtime validator actually checks - Simplify RpcDispatcher return type to Promise<Json> (always awaited) - Fix "narrow" → "consolidate the unsafe cast to" in RpcDispatcher JSDoc - Fix missed handlerDefinition() migration in socket-integration test - Reduce asHandler/handlerDefinition JSDoc blocks to single inline comments - Add tests: getStatus/listActions struct rejection, params-absent → -32602, log-on-handler-throw, dispose-error-during-shutdown Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
sirtimid
force-pushed
the
sirtimid/wallet-cli-rpc-handler-structs
branch
from
July 13, 2026 12:33
6dcb9c9 to
99e87a9
Compare
Member
|
@sirtimid actually, since we would have returned different errors prior to this work, we should add a changelog entry for the package. |
… in RpcHandlerMap Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
sirtimid
enabled auto-merge
July 14, 2026 14:00
rekmarks
approved these changes
Jul 14, 2026
sirtimid
added a commit
that referenced
this pull request
Jul 14, 2026
Reconcile the auto-unlock (optional `--password`) work on this branch with main's opaque `Password`/`Srp` daemon-secret types (#8863) and dispatch-time RPC param validation (#8846): - `DaemonSpawnConfig`/`CreateWalletConfig` password is now `password?: Password` (optional) with `srp: Srp`. - daemon-entry wraps the env password only when present (`passwordRaw ? Password.from(passwordRaw) : undefined`); empty/absent stays "start locked". - daemon-spawn unwraps `Password`/`Srp` at the child-env boundary while keeping the strip-inherited-password logic for the no-password case. - wallet-factory drops the now-redundant `blankToUndefined` (opaque `Password` is validated non-empty at construction) and unwraps at the keyring boundary. - start command builds `password` as `Password | undefined` so `--password` stays optional. - Tests updated to opaque types; dropped the empty-string-password cases that can no longer reach `createWallet`. Old `wallet-factory-integration.test.ts` removed in favour of this branch's renamed `wallet-factory.integration.test.ts`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Resolves #8777.
RpcHandler<TParams, TResult>,RpcHandlerDefinition<TParams, TResult>({ paramsStruct, run }), and adefineHandler(paramsStruct, run)helper indaemon/types.ts.RpcHandlerMapnow maps method names to definitions.rpc-socket-server.tsvalidatesparamsagainst the handler's struct viasuperstruct.validatebefore dispatch and returns-32602 invalidParamson shape mismatch. Each handler body can now trust the shape of its argument.daemon-entry.ts:getStatususesliteral(null).calluses adefined struct that enforces a non-empty[string, ...Json[]]tuple, dropping the ad-hocArray.isArray / typeofchecks from the handler body.wallet.messenger.call as anycast with a single typedRpcDispatcher = messenger.call.bind(messenger)escape hatch.@metamask/superstruct ^3.1.0towallet-cli(already used across the monorepo at this version).Test plan
yarn workspace @metamask/wallet-cli run test— 248 tests pass, 100% line/branch/function/statement coverage on the touched files.yarn lint:eslint packages/wallet-cli/src— clean.yarn constraints— clean.yarn workspace @metamask/wallet-cli run changelog:validate— clean.-32602 invalidParamspath and verifiesrunis not invoked when the struct rejects.callhandler cover both the rejection cases (null, empty, non-string head, non-array) and the accepting case.🤖 Generated with Claude Code
Note
Medium Risk
Changes the wallet daemon JSON-RPC dispatch path and how malformed
paramsare surfaced (standard invalidParams vs handler errors), on a local socket that still exposes the full messenger surface.Overview
Daemon RPC handlers are now
{ paramsStruct, run }definitions (viadefineHandler) instead of raw functions.rpc-socket-servervalidatesparamswith superstruct before callingrunand responds with-32602 invalidParamswhen the shape does not match, so handlers no longer need ad-hoc checks.daemon-entrywiresgetStatus/listActionswithliteral(null),callwith a struct for a non-empty[string, ...]action tuple, and routes messenger dispatch through a singleRpcDispatcherbind instead of inline casts. Shutdown now logs and continues ifdispose()fails during teardown.Adds
@metamask/superstructand expands tests for invalid-params paths and struct acceptance/rejection.Reviewed by Cursor Bugbot for commit 0c971a5. Bugbot is set up for automated code reviews on this repo. Configure here.