Skip to content

feat(app): mask secrets in API key and MCP install snippets with a shared RevealSnippet - #2988

Open
brandon-pereira wants to merge 3 commits into
mainfrom
brandon/reveal-snippet-component
Open

feat(app): mask secrets in API key and MCP install snippets with a shared RevealSnippet#2988
brandon-pereira wants to merge 3 commits into
mainfrom
brandon/reveal-snippet-component

Conversation

@brandon-pereira

@brandon-pereira brandon-pereira commented Aug 24, 2026

Copy link
Copy Markdown
Member

What & why

Credentials were rendered in plain text across the app — the Team Settings API-key cards showed the full key, and the MCP install snippets inlined the personal access key directly into the copyable command/JSON. That's a shoulder-surfing risk on a screen-share or in a screenshot.

This adds a shared RevealSnippet component that masks secrets in a snippet until the user explicitly reveals them, while copy always hands back the real value (so nobody has to reveal a key just to copy it). It's adopted across every credential surface touched here:

  • Team Settings — ingestion API key + personal access key now render as masked, read-only fields with an inline reveal-eye and copy icon.
  • MCP install panel — the access key is masked in every host snippet (Claude Code / Codex CLI commands, the Cursor & VS Code deep-link JSON fallback, and the generic JSON block).

Design

RevealSnippet is a compound componentRevealSnippet.Code, .Reveal, .Copy, and a self-contained .Input share reveal state via context, so the surrounding layout (header row, panel chrome, button placement) is entirely up to the caller.

Masking is driven by a single secrets prop, with a deliberately safe default:

secrets behavior
omitted / [] nothing masked — plain copyable snippet
[value] (bare string) whole value masked via defaultRedact (prefix + dots) — for a bare API key
[[real, redacted], …] only those substrings masked — for a secret embedded in a config/prompt

Bare-string and pair entries can be mixed, and null entries are skipped so secrets={[maybeKey]} is safe (e.g. a ClickHouse Cloud deployment with no key). The masked stand-in is padded to the real length so the field width doesn't jump on reveal.

Screenshots

Screenshot 2026-08-24 at 3 09 14 PM

Testing

  • New unit tests for RevealSnippet (masking, reveal toggle, bare-string vs. pair vs. null secrets, canReveal=false, render-prop override) and updated ApiKeysSection tests (masked value on the input, inline reveal).
  • Storybook stories cover every capability (API-key field, single/multiple secrets, MCP install shapes, custom reveal control, reveal-disabled, nullable secret, no secrets).
  • tsc --noEmit, ESLint, and unit tests all pass.

… snippets

Introduce a shared RevealSnippet compound component that masks secrets in
code snippets until revealed, while copy always yields the real value.
Adopt it across the Team Settings API-key fields and the MCP install
snippets (CLI command, deep-link JSON fallback, plain JSON).

Masking is driven entirely by the secrets prop: omitted/empty renders a
plain snippet, a bare string masks the whole value via defaultRedact, and
[real, redacted] pairs mask specific substrings.
@changeset-bot

changeset-bot Bot commented Aug 24, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 4ebe870

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 3 packages
Name Type
@hyperdx/app Patch
@hyperdx/api Patch
@hyperdx/otel-collector Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel

vercel Bot commented Aug 24, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
hyperdx-oss Ready Ready Preview Aug 24, 2026 10:05pm
hyperdx-storybook Ready Ready Preview Aug 24, 2026 10:05pm

Request Review

@github-actions github-actions Bot added the review/tier-3 Standard — full human review required label Aug 24, 2026
@github-actions

github-actions Bot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

🟡 Tier 3 — Standard

Introduces new logic, modifies core functionality, or touches areas with non-trivial risk.

Why this tier:

  • Diff size: 947 production lines changed (Tier 2 max: < 250)

Review process: Full human review — logic, architecture, edge cases.
SLA: First-pass feedback within 1 business day.

Stats
  • Production files changed: 8
  • Production lines changed: 947 (+ 347 in test files, excluded from tier calculation)
  • Branch: brandon/reveal-snippet-component
  • Author: brandon-pereira

To override this classification, remove the review/tier-3 label and apply a different review/tier-* label. Manual overrides are preserved on subsequent pushes.

@greptile-apps

greptile-apps Bot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR introduces a shared reveal-aware credential snippet and adopts it for Team Settings API keys and MCP installation instructions.

  • Masks credential text until an explicit reveal while preserving real-value copying.
  • Re-masks a revealed credential when its value changes.
  • Applies masking to MCP command, JSON fallback, and generic configuration snippets.
  • Adds focused unit tests and Storybook coverage for masking, reveal, copy, nullable secrets, and credential rotation.

Confidence Score: 5/5

The PR appears safe to merge because no blocking failure remains.

No blocking failure remains.

Important Files Changed

Filename Overview
packages/app/src/components/RevealSnippet/RevealSnippet.tsx Implements the compound reveal, redaction, input, code, and copy controls, including resetting reveal state when the value changes.
packages/app/src/components/TeamSettings/ApiKeysSection.tsx Replaces plaintext ingestion and personal-access-key rendering with masked read-only fields.
packages/app/src/components/ClickStackOnboarding/McpInstallPanel.tsx Passes the deployment access key into every human-readable MCP installation snippet for masking.
packages/app/src/components/ClickStackOnboarding/installSnippets.ts Documents the intentional presence of encoded credentials in installation deep links.
packages/app/src/components/RevealSnippet/tests/RevealSnippet.test.tsx Covers masking, revealing, copying, nullable inputs, disabled reveal controls, and re-masking after credential changes.

Reviews (2): Last reviewed commit: "test(app): cover copy-without-reveal + M..." | Re-trigger Greptile

Comment thread packages/app/src/components/RevealSnippet/RevealSnippet.tsx
Comment thread packages/app/src/components/RevealSnippet/RevealSnippet.tsx Outdated
Comment thread packages/app/src/components/RevealSnippet/RevealSnippet.tsx
@github-actions

github-actions Bot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Deep Review

✅ No critical issues found. The previously flagged P1 (reveal state surviving key rotation) is resolved in this branch — RevealSnippet re-masks during render when value changes (RevealSnippet.tsx), with a dedicated regression test. Copy-while-masked, canReveal=false, null secrets, and bare-string vs. pair secrets are all covered. The items below are hardening and standards recommendations only.

🟡 P2 — recommended

  • packages/app/src/components/RevealSnippet/RevealSnippet.tsx:91redact masks by exact substring match on the raw secret, but the Codex snippet passes the key through shellEscape and the JSON blocks through JSON.stringify before rendering, so a key containing ", \, $, or a backtick would appear in escaped form that the raw-value match never masks; not reachable with today's UUIDv4 keys, but the masking guarantee silently depends on that format assumption.
    • Fix: Redact the same rendered representations the snippet builders emit (e.g. also register the shell-escaped and JSON-escaped forms of each secret as redaction targets), and add a test asserting a metacharacter-bearing key stays masked.
    • security, correctness, previous-comments
🔵 P3 nitpicks (1)
  • packages/app/src/components/RevealSnippet/RevealSnippet.tsx:359 — The file is 359 lines, over the documented 300-line component limit in AGENTS.md:92 ("Keep files under 300 lines; break down large components").
    • Fix: Extract the compound subcomponents (Code, Reveal, Input, Copy) or the redaction helpers into sibling modules to bring the file under the limit.
    • project-standards, maintainability, previous-comments

Reviewers (7): correctness, testing, maintainability, project-standards, kieran-typescript, security, previous-comments.

Testing gaps:

  • No test asserts masking holds for an access key containing shell/JSON metacharacters — the escaped-form gap above is currently unverified either way.

Notes: The Cursor/VS Code deep links carry the base64/URL-encoded key in the DOM href and are intentionally left unmasked (documented as residual-by-design in installSnippets.ts); it is not human-readable shoulder-surfable text and is required for one-click install, so it is not treated as a finding. The knip "unused export" flag on defaultRedact is a false positive — it is imported by two test files for assertions; SecretInput and RevealRenderProps are already non-exported in the current diff.

@github-actions

github-actions Bot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

E2E Test Results

All tests passed • 306 passed • 1 skipped • 1229s

Status Count
✅ Passed 306
❌ Failed 0
⚠️ Flaky 2
⏭️ Skipped 1

Tests ran across 4 shards in parallel.

View full report →

- Reset reveal state when the value changes (e.g. API key rotation) so a
  freshly issued credential is never shown without a fresh reveal (P1).
- De-export defaultRedact, SecretInput, RevealRenderProps (knip: unused
  exports); they remain internal.
- Drop an `as HTMLInputElement` cast in the ApiKeysSection test in favor of
  the exact masked-string assertion (which already covers length).
…daction

- Add tests asserting Copy and Input place the REAL value on the clipboard
  while the snippet is still masked (the core copy-without-reveal promise).
- Add McpInstallPanel consumer tests: the access key is masked in both the
  command and JSON-fallback snippets, and revealed only on an explicit action.
- Clamp defaultRedact's visible prefix to half the value so short inputs are
  still masked instead of shown in full.
- Redact longest secrets first so overlapping real values are order-independent.
- Re-export defaultRedact and use it in the ApiKeysSection test instead of a
  local re-implementation.
- Document the intended residual: MCP deep links carry the (encoded) key in
  their href by necessity; only the JSON fallback is reveal-gated.
// Shared muted-at-rest treatment for the Reveal and Copy buttons. Uses the
// themed text color (not Mantine "dimmed") so it stays legible in light/dark
// and across brands; both buttons carry `c="inherit"` to pick up this color.
.reveal {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would setting variant="link" not also give us this?

}

return (
<Button

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OOC what would happen if someone triple clicked the text? I assume that wouldn't copy with the plaintext, but instead have the ***** ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

review/tier-3 Standard — full human review required

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants