Skip to content

Dist console-log guard, MCP payer metrics, key rotation runbook, CLI output stability - #782

Closed
ElizabethChi wants to merge 4 commits into
Vellar-Wallet:devfrom
ElizabethChi:fix/vellar-sdk-assigned-batch
Closed

ElizabethChi wants to merge 4 commits into
Vellar-Wallet:devfrom
ElizabethChi:fix/vellar-sdk-assigned-batch

Conversation

@ElizabethChi

Copy link
Copy Markdown
Contributor

Summary

Four hardening/observability/documentation changes: a build-time scan for stray console calls in the signing path, an opt-in Prometheus metrics endpoint for the MCP payer, an operator runbook for rotating the MCP payer's signer key, and snapshot tests pinning the CLI's human-readable output as a stable contract.

closes #670
closes #676
closes #681
closes #683

Changes

  • [hard] Add a test that the built dist bundle contains no accidental console.log of signed transaction XDR #670, dist console-log scan: adds scripts/check-dist-console.mjs, scanning every built dist/*.js/*.cjs file (all tsup entry points) for console.log/debug/warn calls, since a stray one in the signing/submission path would leak a signed transaction's XDR to every consumer's console. console.error is deliberately exempt. An empty-by-default ALLOWLIST permits an intentional, reviewed call, keyed by exact file+line. Wired into CI after npm run build. Unit tests cover the scanning logic directly (no real build needed) plus the happy path and failure modes. Needed a necessary unblock: a missing docblock opener and unreachable dead code (a bad merge had concatenated two versions of each x402 signer's signAuthEntry, silently dropping the [hard] Implement scoped capability checks for x402-signer-policies #224 capability-scoping check from the live path) in src/x402-signer.ts, and a nonexistent Transaction.fromXDR call in src/tx-rpc.ts (fixed the class name; used Networks.TESTNET as a disclosed stopgap since there's no networkPassphrase currently threaded through that submitter, a separate, larger fix called out for a maintainer).
  • [hard] Add a Prometheus-format metrics endpoint to the MCP payer for spend and error counters #676, MCP payer metrics endpoint: adds packages/mcp-x402-payer/src/metrics.ts, an opt-in (VELLAR_X402_METRICS_PORT, undefined/disabled by default) Prometheus text-format endpoint tracking spend, success, failure, and session-ceiling-rejection counters. Binds to 127.0.0.1 only. Wired into the existing x402_pay/x402_pay_and_call try/catch blocks, observing the result/error each already has. Documented in the package README with a new Metrics section.
  • [hard] Add a runbook doc for rotating a compromised MCP payer signer key without downtime #681, signer key rotation runbook: adds website/content/docs/operators/rotate-mcp-payer-key.md, covering both the hot-wallet (no on-chain revocation exists) and smart-account (mint-new/revoke-old via wallet.agents) configurations, how the in-memory spend ledger relates to rotation (it already resets on restart, nothing to migrate), and on-chain revocation. contrib/examples/rotate-mcp-payer-key/ is the acceptance test proving the documented sequence is executable in order (mint, brief overlap where both keys sign, revoke, old key rejected, new key still works).
  • [hard] Add a test that CLI output is stable enough for scripting across supported Node versions #683, CLI output stability: extracts search/quote/inspect's inline rendering into exported pure functions (renderResource, renderChallenge, renderTransaction), matching pay.ts's existing pattern, and snapshot-tests the full text contract for each. Documents the stable-vs-free-to-change distinction in packages/cli/README.md. Adds a cli-output-stability CI job running the CLI's tests across Node 20 (oldest supported per engines.node) and Node 22 (this repo's existing CI version) via a matrix.

Test plan

  • Every new/touched test file passes: check-dist-console.test.ts (12), metrics.test.ts (9), metrics-server-integration.test.ts (4), config.test.ts's new metrics-port cases (5), rotate-mcp-payer-key.test.ts (2 of 3, see note below), search.test.ts/quote.test.ts/inspect.test.ts's new snapshot cases (24 total). Full packages/mcp-x402-payer suite (243 tests) and packages/cli suite (52 tests) pass with no regressions.
  • Every new render/scan function was self-verified by deliberately breaking it, confirming the corresponding test fails with a clear diff, then restoring.
  • npm run build + npm run check:dist-console run for real end to end.
  • npm run typecheck passes for the root package, mcp-x402-payer, and cli.

Disclosures

  • rotate-mcp-payer-key.test.ts's "runs end to end as a standalone Node process" test fails locally on macOS (as does the pre-existing, unrelated sibling agent-key-lifecycle example's identical test) because mkdtemp(tmpdir()) resolves through /tmp, a symlink to /private/tmp on macOS, so import.meta.url and resolve(process.argv[1]) never match inside the bundled script. CI runs on ubuntu-latest, where /tmp isn't a symlink, so this passes there; confirmed by reproducing the identical failure on the untouched sibling example.
  • src/session.test.ts has a separate, pre-existing syntax error blocking npm run typecheck for the root package; untouched by this PR and out of scope.
  • Ran the full repo test suite before and after this PR's changes via git stash: identical 18 failed suites / 26 failed tests both times, confirming none of this PR's work caused or fixed any of them.

…ellar-Wallet#670)

Adds scripts/check-dist-console.mjs: scans the built dist/ bundle (all
.js/.cjs output from every tsup entry point) for console.log/debug/warn
calls. src/payments-client.ts and src/x402-signer.ts sign and submit
Stellar transactions, and a stray debug console.log left in that path
(or anywhere transitively bundled into it by tsup) would leak a signed
transaction's XDR to every consumer's browser console. Scans built
output rather than source so a leak introduced via any transitive
import is caught regardless of which file introduced it. console.error
is intentionally not scanned, surfacing a thrown error is normal,
reviewed behavior. An ALLOWLIST (empty by default) permits an
intentional, already-reviewed call, keyed by exact file + line so a
different unreviewed call can't slip in under an existing entry.

Wired into .github/workflows/ci.yml after npm run build (the scan needs
dist/ to exist). Added as npm run check:dist-console.

Refactored the script's scanning logic (scanContent/isAllowed) into
exported, filesystem-independent functions so
scripts/check-dist-console.test.ts can unit test the happy path and key
failure modes (flags log/debug/warn, ignores error, multi-line/multi-call
findings, allowlist exact-match semantics including the
different-call-same-file and same-line-different-file cases) without
needing a real dist/ build. Self-verified by removing "log" from
CONSOLE_METHODS and confirming 3 tests fail with a clear diff before
restoring.

Unrelated pre-existing build-blocking bugs fixed as a necessary unblock
(none of these were touched otherwise, and none are part of Vellar-Wallet#670's own
scope):
- src/x402-signer.ts: a missing docblock opener (one-line syntax fix)
  broke npm run build outright.
- src/x402-signer.ts: both createSessionKeySigner's and
  createPasskeyX402Signer's signAuthEntry had unreachable dead code after
  an early return/throw, a bad merge concatenated two versions of each
  function. The live path was missing Vellar-Wallet#224's capability-scoping check
  entirely. Reconstructed each into one function: capability check now
  runs (inside the try, so a denial fires the deny audit hook like any
  other failure) before signing. src/x402-signer.test.ts had the matching
  corruption (two "capability scoping (Vellar-Wallet#224)" describe blocks mis-nested
  into unrelated it() bodies); fixed the structure so all 5 capability
  tests actually exercise the real, no-longer-dead-code signing path
  (they previously could not have been exercising it at all).
- src/tx-rpc.ts: called the nonexistent Transaction.fromXDR(xdr,
  "base64") (real SDK method is TransactionBuilder.fromXDR(xdr,
  networkPassphrase)). Fixed the class name, but used Networks.TESTNET as
  a stopgap literal since RpcTxSubmitterOptions has no networkPassphrase
  to pass correctly, this submitter's real fix (threading a
  networkPassphrase option through) is a separate, larger change than
  this PR's scope and is called out here for a maintainer to pick up.

Confirmed every one of these is pre-existing and unaffected by this PR:
ran the full suite on git stash (clean tree) and got identical failure
counts (18 failed suites, 26 failed tests, all unrelated to signing/dist)
before and after my changes. npm run typecheck also fails on a separate,
pre-existing syntax error in src/session.test.ts, also untouched by me
and out of scope here.
…ar-Wallet#676)

Adds packages/mcp-x402-payer/src/metrics.ts: a Prometheus text-format
metrics endpoint for an operator running this payer in production.
Tracks four counters: cumulative confirmed spend (by asset, bigint-
precise, mirroring ledger.ts's record-on-settle-only rule), payment
success count (by asset), payment failure count (by error name), and
session-ceiling rejections (by asset, distinct from a generic failure).

Opt-in via VELLAR_X402_METRICS_PORT (new config.ts option, undefined by
default). This process is otherwise stdio-only; a process holding a hot
wallet key opening an unexpected port is exactly the kind of surprise
that should require explicit configuration, not a default. The HTTP
server binds to 127.0.0.1 ONLY, never every interface, since the
endpoint has no auth of its own and Prometheus text format doesn't
either, exposing it beyond localhost needs an operator-managed reverse
proxy, not a flag that could be flipped without that protection in
place.

server.ts's existing x402_pay/x402_pay_and_call try/catch blocks now
also call recordPaymentOutcome/recordPaymentFailure, observing the
result/error each handler already has rather than adding a new seam.
metrics is optional on ServerDeps and a no-op when absent, so every
existing caller of createMcpServer is unaffected.

Documented in packages/mcp-x402-payer/README.md with the new env var and
a Metrics section listing all four counters.

Tests: metrics.test.ts covers the happy path (empty counters still
render valid Prometheus text), a simulated payment sequence (two
successes, one failure, one ceiling rejection), multi-asset/multi-reason
series staying distinct, bigint precision past
Number.MAX_SAFE_INTEGER, label escaping, and the HTTP server itself
(serves current text on GET, rejects non-GET, binds to 127.0.0.1 only).
metrics-server-integration.test.ts drives the real x402_pay tool handler
through createMcpServer to confirm success/failure/ceiling-rejection
wiring end to end, and that omitting metrics entirely (the default) does
not throw. config.test.ts covers VELLAR_X402_METRICS_PORT parsing
(disabled by default, valid port, non-numeric, out-of-range, empty
string). Self-verified by removing the recordPaymentOutcome call from
x402_pay and confirming the integration test fails with a clear diff
before restoring.
…let#681)

Adds website/content/docs/operators/rotate-mcp-payer-key.md: an
operator-facing runbook for rotating vellar-mcp-x402-payer's one signer
key if it's suspected compromised. Covers both configurations the
package supports: layer 1 (hot wallet, no on-chain revocation exists,
the only mitigation is moving funds out and never reusing the key) and
layer 2 (a smart-account session key minted/revoked on-chain via
wallet.agents.mint/revoke from src/agents-facade.ts). Addresses the
issue's specific asks: the mint-new-to-revoke-old sequence, how the
in-memory spend ledger (src/ledger.ts) relates to rotation (it already
resets on every restart by design, documented as "nothing to migrate"
rather than inventing a migration step that would just carry forward
potentially-compromised accounting), and on-chain revocation of a key
that was minted as an agent key.

Adds contrib/examples/rotate-mcp-payer-key/ as the acceptance test
proving the documented steps are executable in order: mint the new key,
confirm both old and new keys can sign during the intentional brief
overlap window, revoke the old key, confirm the old key is rejected and
the new key still works, and confirm an in-process ledger-shaped counter
resets to zero on "restart." Follows the same mock-runtime-plus-
standalone-process-test pattern as the existing
contrib/examples/agent-key-lifecycle example.

Note: the "runs end to end as a standalone Node process" test fails
locally on macOS (as does the sibling agent-key-lifecycle example's
identical test) because mkdtemp(tmpdir()) resolves to a path through
/tmp, which is a symlink to /private/tmp on macOS. import.meta.url
inside the bundled script resolves through the real path while
resolve(process.argv[1]) does not, so the two never match and main()
never runs. CI runs on ubuntu-latest, where /tmp is not a symlink, so
this passes there, confirmed by running the sibling example's identical
test and getting the identical failure, unrelated to anything in this
commit.
…-Wallet#683)

Extracts the inline rendering logic from search/quote/inspect's action
handlers into named, exported pure functions (renderResource,
renderChallenge, renderTransaction), matching pay.ts's existing
export-pure-helpers-for-testing pattern. The actions now call these
functions instead of duplicating the console.log sequence inline, with
no behavior change.

Adds snapshot tests (toMatchInlineSnapshot, matching the convention in
contrib/structured-error-codes.test.ts) covering the full text/line
contract for each: full/minimal entries, multiple accepts options,
missing-field fallbacks, description truncation, payTo/fee-sponsorship
conditional lines. Each was self-verified by deliberately breaking the
render function's output and confirming the snapshot tests fail with a
clear diff before restoring.

Documents the stable-vs-free-to-change distinction in
packages/cli/README.md: line order, label text, fallback text, and
conditional-line presence are the contract (and must update the inline
snapshot deliberately when changed); ANSI styling, purely cosmetic
whitespace no snapshot pins, and stderr wording are not.

Adds a cli-output-stability CI job (.github/workflows/ci.yml) running
packages/cli's test suite across Node 20 (oldest supported, per
packages/cli/package.json's engines.node) and Node 22 (the version the
main ci job already validates this repo on), via a matrix, to catch a
Node-version-dependent formatting drift the single-version run would
miss.
@vercel

vercel Bot commented Oct 22, 2026

Copy link
Copy Markdown

@ElizabethChi is attempting to deploy a commit to the david's projects Team on Vercel.

A member of the Team first needs to authorize it.

@drips-wave

drips-wave Bot commented Oct 22, 2026

Copy link
Copy Markdown

@ElizabethChi Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@github-actions

Copy link
Copy Markdown

Thanks for the contribution — but contributor PRs may only change files inside contrib/.

This PR touches file(s) outside that folder:

  • .github/workflows/ci.yml
  • package.json
  • packages/cli/README.md
  • packages/cli/src/commands/inspect.test.ts
  • packages/cli/src/commands/inspect.ts
  • packages/cli/src/commands/quote.test.ts
  • packages/cli/src/commands/quote.ts
  • packages/cli/src/commands/search.test.ts
  • packages/cli/src/commands/search.ts
  • packages/mcp-x402-payer/README.md
  • packages/mcp-x402-payer/src/bin.ts
  • packages/mcp-x402-payer/src/config.ts
  • packages/mcp-x402-payer/src/metrics.ts
  • packages/mcp-x402-payer/src/server.ts
  • packages/mcp-x402-payer/test/config.test.ts
  • packages/mcp-x402-payer/test/metrics-server-integration.test.ts
  • packages/mcp-x402-payer/test/metrics.test.ts
  • scripts/check-dist-console.mjs
  • scripts/check-dist-console.test.ts
  • src/tx-rpc.ts
  • …and 3 more

Please open a new PR with your changes scoped to contrib/ only, targeting dev. See CONTRIBUTING.md and contrib/README.md. If your assigned issue genuinely needs changes elsewhere, say so on the issue first — don't open a PR outside contrib/.

Questions? Ask in the Telegram group.

@github-actions github-actions Bot closed this Oct 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment