# Audit log

The account's audit log. Rows hold ids and codes only, so names come from `resolve`; an id it can't resolve (a removed member, say) shows as the id, never a guess. Refusals are rows too, with their reason code. Filter by kind, or to refusals alone. Your app's own actions take their words from `actions`; one with no label shows its code, never a guessed verb.

- Registry name: `audit-log` (`@candor/audit-log`)
- 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/audit-log

## Install

```bash
npx candor-ui add audit-log
```

Run `npx candor-ui@latest init` once first (https://candor.themethodic.com/get-started.md). Without the CLI: `npx shadcn@latest add @candor/audit-log`.
It brings in: locale, team, terms, utils.
npm packages: lucide-react@^1.67.0.

## Example

```tsx
"use client"

import { AuditLog } from "@/components/audit-log"
import type { AuditEvent } from "@/lib/team"

const events: AuditEvent[] = [
  { id: "e1", at: "2026-11-05T09:42:00Z", actorId: "u-priya", action: "approval.approved", target: { type: "approval_request", id: "ap-17" }, outcome: "ok", requestId: "req_9f2c" },
  { id: "e2", at: "2026-11-05T09:20:00Z", actorId: "u-dana", action: "role.set", target: { type: "environment_role", id: "u-jonas" }, outcome: "refused", reasonCode: "NOT_ADMIN_ON_ENVIRONMENT", requestId: "req_77a1" },
  { id: "e3", at: "2026-11-05T08:55:00Z", actorId: "agent-keel", action: "agent.tool_called", target: { type: "tool", id: "run_migration" }, outcome: "ok", requestId: "req_41d0" },
  // The app's own actions: one labelled through `actions`, one not (its code shows as it is).
  { id: "e7", at: "2026-11-05T08:30:00Z", actorId: "u-dana", action: "connection.created", target: { type: "connection", id: "warehouse" }, outcome: "ok", requestId: "req_52aa" },
  { id: "e8", at: "2026-11-05T08:20:00Z", actorId: "system", action: "connection.synced", target: { type: "connection", id: "warehouse" }, outcome: "ok", requestId: "req_52ab" },
  { id: "e4", at: "2026-11-05T08:10:00Z", actorId: "u-priya", action: "member.invited", target: { type: "invite", id: "sam@acme.example" }, outcome: "ok", requestId: "req_31be" },
  { id: "e5", at: "2026-11-04T17:02:00Z", actorId: "system", action: "member.invite_expired", target: { type: "invite", id: "inv_4471" }, outcome: "ok", requestId: "req_0b5e" },
  // A removed member: the id can't be resolved, so it shows as the id.
  { id: "e6", at: "2026-11-04T11:30:00Z", actorId: "u-x91", action: "audit.read", target: { type: "account", id: "acc_acme" }, outcome: "refused", reasonCode: "NOT_OWNER_OR_ADMIN", requestId: "req_a11c" },
]

const names: Record<string, string> = {
  "u-priya": "Priya",
  "u-dana": "Dana",
  "u-jonas": "Jonas",
  "agent-keel": "Keel migrations agent",
  "ap-17": "Add region to customers",
  run_migration: "run_migration",
  warehouse: "Warehouse",
}

export default function Example() {
  return <AuditLog className="w-full max-w-3xl" events={events} resolve={(id) => names[id]} actions={{ "connection.created": { label: "connected", group: "data" } }} />
}
```

## <AuditLog>

Props:

- `events: AuditEvent[]`: The rows, in any order; the newest shows first.
- `resolve: (id: string) => string | undefined`: Turns a principal or target id into a display name, when known.
- `actions?: Record<string, AuditActionInfo> | undefined`: Labels and groups for your app's own actions, by code: `{ "connection.created": { label: "connected", group: "data" } }`. Also overrides a known action's label.
- `className?: string | undefined`

## Strings (`auditLog`)

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

- `label`: Audit log
- `kind`: Kind
- `groupAll`: Everything
- `groupMembers`: People
- `groupRoles`: Roles
- `groupApprovals`: Approvals
- `groupAgent`: Agent
- `groupData`: Data
- `groupSessions`: Sign-ins
- `groupOther`: Other
- `refusalsOnly`: Refusals only
- `empty`: No events match.
- `notAMember`: Not a current member
- `line`: {actor} {action} {target}
- `refused`: was refused: {action}
- `unlabelled`: recorded {action} for
- `request`: request {id}
- `signinSucceeded`: signed in
- `signout`: signed out
- `stepupCompleted`: confirmed their identity
- `accountCreated`: created the account
- `memberInvited`: invited
- `memberInviteRevoked`: revoked the invite for
- `memberInviteExpired`: let expire the invite for
- `memberJoined`: joined
- `memberRemoved`: removed
- `roleSet`: set a role for
- `roleRemoved`: removed a role from
- `approvalRequested`: requested approval
- `approvalApproved`: approved
- `approvalSelfApproved`: self-approved
- `approvalRejected`: rejected
- `approvalWithdrawn`: withdrew
- `approvalExpired`: expired
- `approvalApplied`: applied
- `auditRead`: tried to read the audit log
- `approvalRead`: tried to read approvals
- `agentToolCalled`: called
- `agentApprovalRequested`: asked for the go-ahead to run
- `agentApprovalGranted`: allowed
- `agentApprovalDenied`: turned down
- `agentApprovalExpired`: let expire the go-ahead request to run
- `dataExported`: exported data:
- `dataDeleted`: deleted data:
- `dataRetentionChanged`: changed how long data is kept:

## Types

### AuditActionInfo

How AuditLog shows one of your app's own actions.

```ts
{ /** The verb, read between the actor and the target: "connected", "rotated the key for". */ label: string /** Which filter it falls under. Defaults to the group its prefix names ("member.", "data."), else Other. */ group?: AuditGroup | undefined }
```

## 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` (fixed strings) and `TermsProvider` (nouns); entity kinds and agent roles in `EntityKindsProvider` and `AgentRolesProvider`.
