Skip to content

feat: make personal API access keys rotatable - #2926

Merged
kodiakhq[bot] merged 5 commits into
mainfrom
tom/rotatable-api-tokens
Aug 18, 2026
Merged

feat: make personal API access keys rotatable#2926
kodiakhq[bot] merged 5 commits into
mainfrom
tom/rotatable-api-tokens

Conversation

@teeohhem

@teeohhem teeohhem commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Summary

Personal API access keys can now be rotated from Team Settings → API & Agents.

The key is the bearer token for the external API v2 and the MCP server, but it was generated once at account creation and could never be changed. A leaked key could only be dealt with by deleting the user.

What's new

  • PATCH /me/accessKey, plus a Rotate access key button on the Personal API access key card, mirroring the ingestion key flow directly above it.
  • Rotation is immediate, with no grace period. Your browser session stays signed in.

Two deliberate limits

  • The route takes no user identifier (the id comes from the session), so it can only ever rotate the caller's own key.
  • It is not exposed on the bearer-authed external API v2. A leaked key can already read itself there, so letting it also rotate would let someone lock the owner out of their own tooling.

Also in this diff

  • Both rotate flows now use the shared useConfirm dialog instead of a bespoke modal (review feedback). The ingestion flow loses its modal title as a result, and its confirm/cancel testids become the shared confirm-* ones.
  • APIKeyCopyButton emitted data-test-id, which neither Playwright nor Testing Library queries by default, and both key displays shared the value api-key. Now data-testid, split into ingestion-api-key and personal-access-key.
  • Sentence case applied to the whole card, including the pre-existing ingestion key labels, per agent_docs/code_style.md. Half-converting it would have looked like an oversight. MCP.md references the same labels, so it moves too.
  • Agent docs: a required-pattern section for useConfirm, and a fix to the AGENTS.md pointer that said to read code_style.md "only when actively coding". This PR shipped title-case labels and a hand-rolled modal because that invited deferring the file past planning, with both rules already written down.

How to test on Vercel preview

N/A. Not reachable on the preview build. useMe() returns null under LOCAL_MODE and the Personal API access key card only renders when me != null, so neither the card nor its Rotate button exists there. Covered by integration, component and E2E tests instead.

References

  • Linear Issue: HDX-5099

The personal access key is the bearer token for the external API v2 and the
MCP server, but it was generated once at account creation with no way to
change it, so a leaked key could only be dealt with by deleting the user.

Adds PATCH /me/accessKey plus a Rotate Access Key control on the Personal
API Access Key card, mirroring the existing team ingestion key flow. The
route takes no user identifier (the id comes from the session), so it can
only rotate the caller's own key, and it is deliberately not exposed on the
bearer-authed external API v2. Rotation takes effect immediately and leaves
the browser session signed in.

Also standardizes APIKeyCopyButton on data-testid, which replaces
data-test-id that neither Playwright nor Testing Library queries by
default, and splits the duplicated "api-key" value into ingestion-api-key
and personal-access-key.
@changeset-bot

changeset-bot Bot commented Aug 17, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 9480389

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

This PR includes changesets to release 4 packages
Name Type
@hyperdx/api Patch
@hyperdx/app Patch
@hyperdx/common-utils 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

@github-actions github-actions Bot added the review/tier-4 Critical — deep review + domain expert sign-off label Aug 17, 2026
@github-actions

github-actions Bot commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

🔴 Tier 4 — Critical

Touches authentication, tenancy data models, the public API or shipped database config — or substantially changes the query rendering engine, background tasks, the OTel pipeline, image build, or release CI.

Why this tier:

  • Critical-path files (1) — tenancy, public API, or shipped database config:
    • packages/api/src/routers/api/me.ts
  • Cross-layer change: touches frontend (packages/app) + backend (packages/api) + shared utils (packages/common-utils)

Review process: Deep review from a domain expert. Synchronous walkthrough may be required.
SLA: Schedule synchronous review within 2 business days.

Stats
  • Production files changed: 5
  • Production lines changed: 214 (+ 391 in test files, excluded from tier calculation)
  • Critical-path lines changed: 33
  • Branch: tom/rotatable-api-tokens
  • Author: teeohhem

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

@greptile-apps

greptile-apps Bot commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds immediate personal API access-key rotation through a session-authenticated endpoint and Team Settings UI.

  • Adds the user-key rotation controller, route, shared response type, and integration coverage.
  • Updates the frontend mutation cache directly from the authoritative rotation response.
  • Migrates both rotation confirmations to useConfirm and updates component and E2E coverage.
  • Aligns API-key labels, documentation, and test identifiers with repository conventions.

Confidence Score: 4/5

The PR is not yet safe to merge because rotation controls can still launch overlapping requests and leave the UI displaying a revoked key.

The shared confirmation prevents duplicate resolution of one dialog but does not prevent opening another dialog while the first mutation is pending; concurrent personal-key responses can therefore update the cache out of order.

Files Needing Attention: packages/app/src/components/TeamSettings/ApiKeysSection.tsx and packages/app/src/api.ts

Important Files Changed

Filename Overview
packages/api/src/controllers/user.ts Adds an atomic user access-key replacement using a newly generated UUID.
packages/api/src/routers/api/me.ts Adds a session-scoped endpoint that rotates only the authenticated user's access key.
packages/app/src/api.ts Adds the rotation mutation and authoritative cache update, but concurrent successful responses remain unordered.
packages/app/src/components/TeamSettings/ApiKeysSection.tsx Adds personal-key rotation and shared confirmations, but the enabled triggers still permit overlapping rotations.
packages/common-utils/src/types.ts Adds the shared response schema and type for personal access-key rotation.
packages/api/src/routers/api/tests/me.int.test.ts Covers authentication, persistence, revocation, session continuity, and user scoping for the new endpoint.

Sequence Diagram

sequenceDiagram
  participant User
  participant UI as Team Settings
  participant API as PATCH /me/accessKey
  participant DB as MongoDB
  participant Cache as React Query me cache
  User->>UI: Confirm rotation
  UI->>API: Session-authenticated PATCH
  API->>DB: Replace User.accessKey
  DB-->>API: Updated user
  API-->>UI: newAccessKey
  UI->>Cache: Update accessKey
  Cache-->>User: Display new key
Loading

Reviews (5): Last reviewed commit: "Merge branch 'main' into tom/rotatable-a..." | Re-trigger Greptile

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

github-actions Bot commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Deep Review

✅ No critical issues found. This is a small, well-tested change: the rotation route takes the user id from the session (never the request), key generation and CSRF posture match the pre-existing PATCH /team/apiKey rotation, the testid rename has no dangling references, and the confirm/cancel flow is exercised by unit, component, and E2E tests. The items below are recommendations and nits only.

🟡 P2 -- recommended

  • packages/api/src/routers/api/me.ts:50 -- The credential-rotation handler emits no audit log, metric, or setBusinessContext call, so an immediate, irreversible revocation of a user's API/MCP bearer token leaves no trace, and AGENTS.md requires team/user-scoped operations to carry business context and countable events to emit a metric.
    • Fix: Add setBusinessContext and an audit log line plus a counter metric for the rotation before returning the response.
    • reliability, project-standards, security
  • packages/api/src/routers/api/__tests__/me.int.test.ts:1 -- No test locks in the deliberate security boundary that PATCH /me/accessKey is unreachable on the bearer-authed external API v2 mount, so a future refactor that mounts the route there would silently reintroduce the lock-out risk the design avoids.
    • Fix: Add an integration test asserting the rotate verb returns 404/405 when reached through the /api/v2 bearer surface.
    • security, testing
🔵 P3 nitpicks (3)
  • packages/app/src/components/TeamSettings/ApiKeysSection.tsx:64 -- onRotateTeamApiKey and onRotateAccessKey are near-identical confirm-then-mutate-then-notify handlers, and the duplication will drift as one flow gains behavior the other lacks.
    • Fix: Extract a shared helper parameterized by message, mutation, and success copy.
  • packages/app/src/api.ts:281 -- The onSuccess cache seed silently no-ops when the me cache entry is null, dropping the new key so a consumer that lost its cache mid-rotation would render the now-revoked key until a reload.
    • Fix: Fall back to a me refetch (or full cache set) when prev == null instead of returning it unchanged.
  • packages/api/src/routers/api/me.ts:57 -- A null result from rotateUserAccessKey (user deleted between auth and update) is raised as a bare Error, surfacing as a 500 rather than a 4xx for a client-observable not-found condition.
    • Fix: Throw Api404Error (or equivalent) so the deleted-user case returns a 4xx.

Reviewers (9): correctness, security, adversarial, api-contract, kieran-typescript, reliability, testing, maintainability, project-standards.

Testing gaps: The confirm (accept) path of rotation is covered only at the integration layer, not E2E, by deliberate design (a confirmed rotation in a parallel worker would 401 the shared-account bearer specs) -- acceptable given the documented constraint.

@github-actions

github-actions Bot commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

E2E Test Results

All tests passed • 300 passed • 1 skipped • 1122s

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

Tests ran across 4 shards in parallel.

View full report →

…tion

Two review findings on the new personal access key flow, plus the sentence-case
convention the card was not following.

The success handler discarded the newAccessKey from the response and relied on
refetchMe() to pick it up. If that refetch failed, every useMe consumer kept
rendering the old key, which the rotation had already revoked, so the user
could copy a dead credential with no way to reach the working one short of a
reload. The mutation now seeds the me cache from the response instead, which
needs no network round trip and removes the failure window.

Confirming closed the modal but left the controls enabled, and Mantine keeps
modal content mounted through the exit transition, so a fast double click sent
two PATCHes and the second revoked the key the first had just generated. The
shared confirm modal now takes confirmDisabled, wired to isPending for both the
ingestion and personal flows.

Applies sentence case to every label in the card per agent_docs/code_style.md,
including the pre-existing ingestion key strings, so the two halves match.
Acronyms keep their casing, so "Rotate API key" and "Rotate personal API access
key". The E2E label assertions and the MCP.md references move with them.
@vercel

vercel Bot commented Aug 17, 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 18, 2026 3:19pm
hyperdx-storybook Ready Ready Preview Aug 18, 2026 3:19pm

Request Review

pulpdrew
pulpdrew previously approved these changes Aug 17, 2026

@pulpdrew pulpdrew left a comment

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.

LGTM with a question/suggestion

);
}

function RotateKeyConfirmModal({

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.

nit: Are we able to use useConfirm here instead of a new component? I do see this is just an extraction of what was there previously for the ingestion key, so not a huge deal.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good call. Will change

Comment on lines +230 to +232
Rotating your personal access key immediately revokes the
current one and generates a new one. This action is{' '}
<b>not reversible</b>.

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.

Not a blocker here, just a callout before EE merge: If I understand this correctly, the access key is not scoped per-team, so will be revoked across teams. This might not be an issue in OSS (no cross-team users), nor Cloud (no access keys), but if anyone is using multiple teams in the private image, this could unexpectedly rotate their key in a different team than the one they're viewing. It could be worth updating this copy in the EE repo to reflect that.

Review feedback: use the existing useConfirm rather than a bespoke modal
component. Both the ingestion and personal key flows now go through it, so
RotateKeyConfirmModal is gone.

This also removes the confirmDisabled/isPending plumbing added for the
double-rotation guard. useConfirm resolves its promise exactly once, so a
double click on Confirm during the modal's exit transition cannot fire a second
PATCH; the guarantee is structural rather than a prop each caller has to wire.

Tradeoff: useConfirm passes no title to the Modal and renders its body at
size="sm" opacity={0.7}, so the ingestion dialog loses its heading and both
warnings are muted. That matches the other four consumers, which are all
destructive delete confirmations. Adding an optional title to useConfirm would
be a separate change across all of them.

The per-flow confirm and cancel testids collapse into the shared confirm-*
ones, so the page object drops its duplicated locators.
Comment thread packages/app/src/components/TeamSettings/ApiKeysSection.tsx
Adds a required-pattern section for useConfirm to code_style.md, next to the
other mandated components. Beyond the usage example it records the parts that
are not obvious from the source: the promise resolves exactly once so double
click protection is free, the confirm/cancel test ids are shared and must not
be duplicated per flow, and component tests have to mock it because
ConfirmProvider pulls in next/router. It also documents the missing title and
the muted body, with the instruction to extend useConfirm rather than fork a
one-off modal.

The AGENTS.md pointer said to read code_style.md "only when actively coding",
which invites deferring it during planning and then never returning. This PR
shipped title-case labels and a hand-rolled modal for exactly that reason, with
both rules already written down. It now says to read it before writing or
planning any packages/app UI change, and calls out that these patterns are
invisible from the surrounding file, so matching the component you are editing
is not sufficient.

@pulpdrew pulpdrew left a comment

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.

LGTM

@kodiakhq
kodiakhq Bot merged commit 9f640a6 into main Aug 18, 2026
27 checks passed
@kodiakhq
kodiakhq Bot deleted the tom/rotatable-api-tokens branch August 18, 2026 15:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

automerge review/tier-4 Critical — deep review + domain expert sign-off

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants