# Api keys

The account's API keys: each one's name, the part of it that's safe to show, what it can do, when it was created and last used, and when it expires. Creating a key asks for a name and its scopes (none is ticked for you), then shows the secret once with a copy button and says it won't be shown again. Revoking asks first and says what breaks: anything using the key is refused from its next request, plus what the app knows uses it.

Focus follows the work: to the name when the form opens, to the "won't be shown again" note when the key is made (so a screen reader reads the note, never the secret), and back to Create key after Cancel or "I've stored it". Keep it returns focus to the key's Revoke button; after a revoke it goes to the API keys heading, since the key's row goes. A key whose `expiresAt` has passed says "expired", checked against the clock as the page stays open; on the server, and until the page has hydrated, it shows only the date.

- Registry name: `api-keys` (`@candor/api-keys`)
- Tier: Pro. Installing it needs a Pro, Premium or Team licence key: the person runs `npx candor-ui key <key>` themselves; never ask for the key in chat or write it to a file.
- Docs: https://candor.themethodic.com/docs/api-keys

## Install

```bash
npx candor-ui add api-keys
```

Run `npx candor-ui@latest init` once first (https://candor.themethodic.com/get-started.md). Without the CLI: `npx shadcn@latest add @candor/api-keys`.
It brings in: alert-dialog, button, checkbox, copy-field, data, input, locale, locked, page-state, relative-time, tokens, utils.
npm packages: lucide-react@^1.67.0.

## Example

```tsx
"use client"

import * as React from "react"
import { ApiKeys, type ApiKey } from "@/components/candor/api-keys"

const ago = (h: number) => new Date(Date.now() - h * 3_600_000).toISOString()
const scopes = [
  { id: "runs:read", label: "Read runs", description: "Runs, their logs and receipts" },
  { id: "runs:write", label: "Start runs" },
  { id: "approvals", label: "Answer approvals" },
]

export default function Example() {
  const [keys, setKeys] = React.useState<ApiKey[]>([
    { id: "k1", name: "Nightly export", hint: "ck_live_…4f2a", scopes: ["runs:read"], createdAt: ago(24 * 40), createdBy: "Ana Ruiz", lastUsedAt: ago(6) },
    { id: "k2", name: "CI pipeline", hint: "ck_live_…91bc", scopes: ["runs:read", "runs:write"], createdAt: ago(24 * 3), lastUsedAt: null, expiresAt: ago(-24 * 87) },
  ])
  return (
    <ApiKeys
      className="max-w-xl"
      keys={keys}
      scopes={scopes}
      onCreate={async (name, chosen) => {
        await new Promise((r) => setTimeout(r, 400))
        setKeys((k) => [{ id: name, name, hint: "ck_live_…7d03", scopes: chosen, createdAt: new Date().toISOString(), lastUsedAt: null }, ...k])
        return "ck_live_example_7d03"
      }}
      onRevoke={(id) => setKeys((k) => k.filter((x) => x.id !== id))}
      revokeImpact={(k) => (k.id === "k1" ? "The nightly export job uses it." : undefined)}
    />
  )
}
```

## <ApiKeys>

Props:

- `keys: ApiKey[]`: The keys, newest first as the app orders them.
- `scopes: ApiKeyScope[]`: Everything a key can be allowed to do.
- `onCreate?: ((name: string, scopes: string[]) => Promise<string>) | undefined`: Shows Create key. Called with the name and the scope ids ticked; resolve with the new secret, which is shown once.
- `onRevoke?: ((id: string) => void | Promise<void>) | undefined`: Shows Revoke on each key. Called with its id once the person confirms; the dialog waits on a returned promise. If it throws or rejects, the dialog stays open and says it failed.
- `revokeImpact?: ((key: ApiKey) => string | undefined) | undefined`: What revoking this key breaks, in the app's words ("The nightly export job uses it."), shown in the confirmation.
- `error?: string | undefined`: Why the last create failed, in the app's words, shown in the form.
- `state?: ListState | undefined`: What the list is showing: its rows (ready, the default), loading, empty or error with the reason, or partial with a note over the rows. Loading, empty and error draw in place of the rows, in this component's own words, so a list the app doesn't have yet isn't shown as an empty one.
- `onRetry?: (() => void) | undefined`: Shows Try again when `state` is an error; called when it's pressed. Pass it only when it loads the list again.
- `className?: string | undefined`

## Strings (`apiKeys`)

The fixed words, English by default. Override any of them under `apiKeys` in `<LocaleProvider strings={{ apiKeys: { … } }}>`; the table is exported as `apiKeysStrings`.

- `label`: API keys
- `heading`: API keys
- `create`: Create key
- `empty`: No keys yet.
- `loading`: Loading API keys
- `error`: API keys couldn’t be loaded.
- `name`: Name
- `namePlaceholder`: Nightly export
- `scopes`: What it can do
- `createIt`: Create
- `creating`: Creating…
- `cancel`: Cancel
- `needsName`: Name the key first.
- `needsScope`: Choose at least one thing it can do.
- `shownOnce`: Copy this key now and keep it somewhere safe. It won’t be shown again.
- `secret`: New key: {name}
- `stored`: I’ve stored it
- `created`: created {when}
- `createdBy`: created {when} by {name}
- `lastUsed`: last used {when}
- `neverUsed`: never used
- `notRecorded`: last use not recorded
- `expires`: expires {date}
- `expired`: expired {date}
- `expiry`: expiry {date}
- `revoke`: Revoke
- `revokeKey`: Revoke {name}
- `revokeTitle`: Revoke {name}?
- `revokeBreaks`: Anything using {name} ({hint}) is refused from its next request.
- `revokeIrreversible`: A revoked key can’t be restored. Create a new key and update whatever used this one.
- `keep`: Keep it
- `createFailed`: Creating the key failed.
- `revokeFailed`: Revoking failed. Try again, or keep the key.
- `keepLocked`: Revoking is under way, so the key can’t be kept now. If it fails, you can keep it.

## Types

### ApiKey

One API key as the app lists it. The secret itself is never here: it's shown once, when the key is created.

```ts
{
  id: string;
  name: string;
  /** The part that's safe to show again, e.g. "ck_live_…4f2a". */
  hint: string;
  /** Scope ids from `scopes`. */
  scopes: string[];
  createdAt: string;
  /** Who created it, by name. */
  createdBy?: string | undefined;
  /** When it was last used; `null` if never. Leave it out when the app doesn't record use, and the row says so. */
  lastUsedAt?: string | null | undefined;
  /** When it stops working on its own. Leave it out for a key that doesn't expire. */
  expiresAt?: string | undefined;
}
```

### ApiKeyScope

Something a key can be allowed to do.

```ts
{
  id: string;
  /** What it allows, e.g. "Read runs". */
  label: string;
  /** A second line, e.g. "Runs, their logs and receipts". */
  description?: string | undefined;
}
```

## Types from other items

The props above take these. Each is defined by another item; import it from the path shown.

### ListState

Import: `import type { ListState } from "@/lib/candor/data"` (item `data`, https://candor.themethodic.com/docs/data.md)

What a list or table is showing, the same shape as `ChartState`: its rows (ready), the shape of rows still coming (loading), nothing and why (empty), the rows it has with a note on what's missing (partial), or why it couldn't load (error). Lists without a `state` show their rows.

```ts
ChartState
```

### ChartState

Import: `import type { ChartState } from "@/lib/candor/data"` (item `data`, https://candor.themethodic.com/docs/data.md)

```ts
{
  kind: "ready";
} | {
  kind: "loading";
}
/** Nothing to plot, and why. */
 | {
  kind: "empty";
  reason: string;
}
/** Some of the range is missing. */
 | {
  kind: "partial";
  note: string;
} | {
  kind: "error";
  reason: string;
}
```

## Rules for using Candor UI

- Components take plain typed data. Never pass model output as HTML or render it as a link; messages are typed blocks.
- A control that can't be used says why: wrap it in `<Locked reason="…">`, don't just disable it.
- Don't show a value you don't have: leave it out (it renders as —) rather than guess. An estimate is labelled as one.
- Nothing is pre-selected in a decision a person makes, and the agent is named from your app's data, never from model output.
- Your app's words go in `LocaleProvider`: its fixed strings in `strings`, its nouns in `nouns`; entity kinds and agent roles in `EntityKindsProvider` and `AgentRolesProvider`.
