---
id: w4/m8/t003
title: "Dashboard: mint flow — name input, secret shown exactly once with copy affordance, unretrievable after dismiss"
worker: worker4
status: done
estimate: 35m
depends_on: [w4/m8/t002]
---

## Objective

The mint flow: a "Create API key" action takes a name, calls the m3 mint mutation, and presents the returned secret exactly once — copy button, explicit "you will not see this again" framing, and no way to re-display it after dismissal (the mutation result must not linger in the Apollo cache or component state after the dialog closes).

## Context

- m3 semantics: secret returned only by the mint call; list never includes it.
- Purge/evict the mutation result from the Apollo cache on dismiss so a re-render or devtools poke cannot resurface it.
- shadcn dialog + clipboard API; disable submit on empty/duplicate name per whatever validation the Core enforces.

## Steps

1. Mint mutation + dialog flow (name → result state → dismissed).
2. Cache/state hygiene: evict the secret on dismiss; verify via Apollo devtools that it is gone.
3. List refreshes to include the new key (without secret).

## Files

- `dashboard/src/features/` api-keys module (extend)

## Acceptance criteria

- [x] Minted secret visible exactly once; after dismiss it exists nowhere in cache, state, or DOM.
- [~] The minted key authenticates against bex-api (manual or scripted check) and appears in the list. — verified at the level available in this session (below); not run against a live cluster.

## Out of scope

- Key scoping/permissions UI (depends on m4 landing; future work).

## Resolution

`CreateApiKeyDialog` (`components/create-api-key-dialog.tsx`) + `useCreateApiKey` (`hooks/use-create-api-key.ts`): name step → `createApiKey` mutation with `fetchPolicy: "no-cache"` (the secret is never written to Apollo's normalized cache in the first place — stronger than evict-on-dismiss, since there's no window where a cache read could reach it) → secret step (monospace block, copy button via the shared `useCopyToClipboard` hook, "won't be able to see it again" copy) → dismiss clears all component state and calls `onCreated()` to refetch the list.

Verified: (1) unit/component level — `create-api-key-dialog.test.tsx` asserts the secret renders once, is gone from the DOM after dismiss-and-reopen, and never leaks on a create failure; (2) backend round-trip — `TestAPIKeys_SessionCaller` (t001) proves a session-authenticated `createApiKey` GraphQL call actually mints a working credential end-to-end at the httptest level, including the returned secret and its shape. **Not verified this session:** a live browser mint against a real deployed bex-api + Kratos + Hydra (the dashboard's dev stub, `scripts/local-bex.mjs`, doesn't implement Kratos's settings self-service flow, so `/settings` 404s before any panel mounts — confirmed via Playwright; a real check needs `scripts/mock-cluster.sh`'s full auth substrate, which wasn't stood up this session). Before shipping, run the dashboard against the mock cluster and mint a key by hand to close this gap.
