Skip to content

refactor(wallet-cli): enforce RPC param validation at dispatch time via superstruct#8846

Open
sirtimid wants to merge 12 commits into
mainfrom
sirtimid/wallet-cli-rpc-handler-structs
Open

refactor(wallet-cli): enforce RPC param validation at dispatch time via superstruct#8846
sirtimid wants to merge 12 commits into
mainfrom
sirtimid/wallet-cli-rpc-handler-structs

Conversation

@sirtimid

@sirtimid sirtimid commented May 18, 2026

Copy link
Copy Markdown
Member

Summary

Resolves #8777.

  • Adds RpcHandler<TParams, TResult>, RpcHandlerDefinition<TParams, TResult> ({ paramsStruct, run }), and a defineHandler(paramsStruct, run) helper in daemon/types.ts. RpcHandlerMap now maps method names to definitions.
  • rpc-socket-server.ts validates params against the handler's struct via superstruct.validate before dispatch and returns -32602 invalidParams on shape mismatch. Each handler body can now trust the shape of its argument.
  • daemon-entry.ts:
    • getStatus uses literal(null).
    • call uses a defined struct that enforces a non-empty [string, ...Json[]] tuple, dropping the ad-hoc Array.isArray / typeof checks from the handler body.
    • Replaces the wallet.messenger.call as any cast with a single typed RpcDispatcher = messenger.call.bind(messenger) escape hatch.
  • Adds @metamask/superstruct ^3.1.0 to wallet-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.
  • New server test covers the -32602 invalidParams path and verifies run is not invoked when the struct rejects.
  • New struct tests on the call handler cover both the rejection cases (null, empty, non-string head, non-array) and the accepting case.

🤖 Generated with Claude Code


Note

Medium Risk
Changes how the Unix-socket daemon validates and dispatches RPC params (including the full messenger call surface); behavior shifts from handler-thrown errors to -32602 for bad shapes, with filesystem permissions still the main access control.

Overview
Daemon JSON-RPC handlers are now { paramsStruct, run } definitions (via defineHandler) instead of plain async functions. rpc-socket-server validates incoming params with @metamask/superstruct before calling run, returning JSON-RPC -32602 invalidParams when validation fails and skipping the handler.

getStatus and listActions expect null params; call uses a custom struct for a non-empty [action, ...args] tuple, removing in-handler array checks. Messenger dispatch is centralized through a single RpcDispatcher bind/cast on wallet.messenger.call.

Shutdown now logs and continues if dispose() throws (same pattern as handle.close()). Tests were updated for the new handler shape and cover struct rejection, -32602, and dispose errors during shutdown.

Reviewed by Cursor Bugbot for commit 6dcb9c9. Bugbot is set up for automated code reviews on this repo. Configure here.

@sirtimid sirtimid force-pushed the sirtimid/wallet-cli-rpc-handler-structs branch from 17d10e8 to c591c6e Compare July 1, 2026 11:31
@sirtimid sirtimid changed the base branch from rekm/wallet-cli to sirtimid/wallet-cli-daemon-e2e July 1, 2026 11:31
@sirtimid sirtimid force-pushed the sirtimid/wallet-cli-daemon-e2e branch 2 times, most recently from 9b0489c to 56d3cf9 Compare July 1, 2026 14:02
Base automatically changed from sirtimid/wallet-cli-daemon-e2e to main July 1, 2026 14:11
sirtimid and others added 9 commits July 2, 2026 10:59
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>
@sirtimid sirtimid force-pushed the sirtimid/wallet-cli-rpc-handler-structs branch from c591c6e to 6cf1fde Compare July 2, 2026 09:56
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>
@sirtimid sirtimid marked this pull request as ready for review July 2, 2026 10:12
@sirtimid sirtimid requested review from a team as code owners July 2, 2026 10:12
@sirtimid sirtimid temporarily deployed to default-branch July 2, 2026 10:12 — with GitHub Actions Inactive
@sirtimid sirtimid changed the title refactor(wallet-cli): Parameterise RpcHandler with struct-validated dispatch refactor(wallet-cli): enforce RPC param validation at dispatch time via superstruct Jul 2, 2026
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

wallet-cli: Parameterise RpcHandler with struct-validated dispatch

1 participant