The redaction checked only top-level keys. That was sufficient while values
were strings — a secret could not hide beneath one — but #1910 lets a value be
an object, so `{ trace: { accessToken: "…" } }` printed in full: `trace` is not
sensitive, so the whole subtree went out with it. `servers/show` output is
meant to be safe to paste into an issue, so this is a regression this PR
introduced rather than a pre-existing gap.
`redactMetadataValue` walks the payload: a key matching `isSensitiveHeader`
replaces its entire value (so a sensitive key holding an object cannot leak
through its members either), anything else is descended into, and arrays are
mapped elementwise since objects inside them can carry sensitive keys.
Recursion is bounded — the payload came from `JSON.parse`, so it is a finite
tree with no cycles.
Tests cover a secret two levels under a non-sensitive key and one inside an
array element.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0177mPHAdECD18nLLCTwR5rh
Signed-off-by: cliffhall <cliff@futurescale.com>
Most tests import runCli()in-process (see helpers/cli-runner.ts) so
clients/cli/src is measured under the coverage gate. Suite-wide
helpers/mock-open-url.ts (vitest setupFiles) mocks open-url so an armed
interactive OAuth path cannot launch a real browser.
e2e.test.ts (and root scripts/smoke-cli.mjs) spawn the built binary for
shebang / process.exit paths — pretest builds test-servers + the CLI
bundle first.
Scripts (from clients/cli/)
npm test# pretest build + all tests
npm run test:cli # subset: cli.test.ts
npm run test:cli-tools # subset: tools.test.ts
npm run test:cli-headers # subset: headers.test.ts
npm run test:cli-metadata # subset: metadata.test.ts
npm run test:coverage # build + ≥90 per-file coverage gate
npm run validate # format:check && lint && typecheck && test