> ## Documentation Index
> Fetch the complete documentation index at: https://docs.lerian.studio/llms.txt
> Use this file to discover all available pages before exploring further.

# Governance

> Manage actor PII mappings, list audit-log archives and download archive objects, and re-verify Matcher's tamper-evident audit hash chain.

Matcher's governance surface groups three capabilities under `/v1/governance`:

* **Actor mappings**: link opaque actor IDs to PII, with pseudonymize and delete operations.
* **Archives**: list completed audit-log archives and download archive objects from object storage.
* **Audit logs**: immutable, hash-chained history with a read-only integrity check.

With `AUTH_PROVIDER=plugin-auth` in multi-tenant mode, tenant identity comes from the JWT. Matcher rejects startup when `MULTI_TENANT_ENABLED=true` and `PLUGIN_AUTH_ENABLED=false`.

<Note>Every governance route stays inside the caller's tenant. Actor-mapping reads have two authorization tiers: the list, which omits `displayName` and `email`, and the single-record de-anonymization read. This separation keeps identity resolution apart from browse access.</Note>

## Actor mappings

***

An actor mapping links an opaque `actorId` (for example `user:550e8400-e29b-41d4-a716-446655440000`) to human-readable PII (`displayName`, `email`). Outside local, development, and test environments, set `ACTOR_PII_ENCRYPTION_KEY` to a base64-encoded 32-byte key before you use actor mappings. If you leave it unset, Matcher continues to run. PII-bearing mapping operations (upsert, single-record read, and pseudonymization) then return an encryptor-required error. The PII-free list and delete paths need no encryptor. Matcher never stores mapping PII in plaintext.

List rows omit the mapping PII fields (`displayName`, `email`) **by design**, but they do return the `actorId` itself. On upsert, Matcher trims leading and trailing whitespace and rejects empty or whitespace-only IDs and IDs longer than 255 characters. Matcher does not impose an opaque-ID format and does not redact the value. An `actorId` that itself contains PII, such as an email address, appears in list rows as its stored value. Pseudonymization keeps that value. Use opaque identifiers if list access must stay PII-free.

The `PUT` response and the single-record `GET` return cleartext identity. The `deanonymize` permission gates only the single-record `GET`. Write access alone gates the `PUT` response, which echoes the full stored record, including any stored field the caller did not submit. Treat actor-mapping write access as PII-revealing. Audit logs can retain the raw `actorId`, which can be an email address.

### List actor mappings

Cursor-paginated rows that omit `displayName` and `email`. Filter by an actor-ID prefix.

```bash theme={null}
curl -X GET "https://api.matcher.example.com/v1/governance/actor-mappings?actorId=user:&limit=25" \
  -H "Authorization: Bearer $TOKEN"
```

```json theme={null}
{
  "items": [
    {
      "actorId": "user:550e8400-e29b-41d4-a716-446655440000",
      "createdAt": "2026-01-15T10:30:00Z",
      "updatedAt": "2026-01-15T10:30:00Z"
    }
  ],
  "limit": 25
}
```

Query parameters: `actorId` (prefix filter), `limit` (default 25, capped at 100), and `cursor`.

### Upsert an actor mapping

Creates or updates the PII for an actor ID. `PUT` is idempotent. The same call creates the record on first use and updates it thereafter. Supply at least one of `displayName` or `email`.

```bash theme={null}
curl -X PUT "https://api.matcher.example.com/v1/governance/actor-mappings/{actorId}" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "displayName": "John Doe",
    "email": "john.doe@example.com"
  }'
```

```json theme={null}
{
  "actorId": "user:550e8400-e29b-41d4-a716-446655440000",
  "displayName": "John Doe",
  "email": "john.doe@example.com",
  "createdAt": "2026-01-15T10:30:00Z",
  "updatedAt": "2026-01-15T10:30:00Z"
}
```

### Get one actor mapping (de-anonymize)

Returns the cleartext PII for a single actor ID. This **is** the de-anonymization primitive, so the narrower `deanonymize` permission gates it rather than plain read.

```bash theme={null}
curl -X GET "https://api.matcher.example.com/v1/governance/actor-mappings/{actorId}" \
  -H "Authorization: Bearer $TOKEN"
```

### Pseudonymize

Replaces the mapping's `displayName` and `email` with `[REDACTED]` while preserving the record and its `actorId` link. This scrubs PII from the mapping only. Immutable audit records keep the raw `actorId` from the original write, and that value can itself be an email address. Historical audit logs and archived files keep their original values. Responds `204 No Content`.

```bash theme={null}
curl -X POST "https://api.matcher.example.com/v1/governance/actor-mappings/{actorId}/pseudonymize" \
  -H "Authorization: Bearer $TOKEN"
```

### Delete a mapping

Permanently removes the mapping. Responds `204 No Content`.

```bash theme={null}
curl -X DELETE "https://api.matcher.example.com/v1/governance/actor-mappings/{actorId}" \
  -H "Authorization: Bearer $TOKEN"
```

<Note>Pseudonymize keeps the record and scrubs its PII. Delete removes the record entirely. Choose pseudonymize when you must retain the audit linkage, delete when the record itself must not persist.</Note>

## Archives

***

The archival worker stays off by default (`ARCHIVAL_WORKER_ENABLED=false`). When you enable it and configure archival storage, the worker compresses aging audit-log partitions and moves them to object storage. Matcher registers the archive retrieval routes whenever archival object storage is available, even when the worker stays off. The list endpoint returns completed archives. The download endpoint issues time-limited URLs for archive objects.

### List archives

Offset-paginated. Filter by date range.

```bash theme={null}
curl -X GET "https://api.matcher.example.com/v1/governance/archives?from=2024-01-01&to=2024-03-31&limit=20&offset=0" \
  -H "Authorization: Bearer $TOKEN"
```

```json theme={null}
{
  "items": [
    {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "partitionName": "audit_logs_2024_q1",
      "dateRangeStart": "2024-01-01T00:00:00Z",
      "dateRangeEnd": "2024-03-31T23:59:59Z",
      "rowCount": 150000,
      "compressedSizeBytes": 10485760,
      "storageClass": "GLACIER",
      "checksum": "sha256:abc123def456...",
      "status": "COMPLETE",
      "archivedAt": "2024-04-01T02:30:00Z"
    }
  ],
  "limit": 20,
  "hasMore": true
}
```

Query parameters: `from`, `to` (`YYYY-MM-DD` or RFC 3339), `limit` (1–200, default 20), and `offset`. The endpoint lists only `COMPLETE` archives and never surfaces in-progress or failed archives.

### Download an archive

Returns a presigned URL plus the checksum for integrity verification.

```bash theme={null}
curl -X GET "https://api.matcher.example.com/v1/governance/archives/{id}/download" \
  -H "Authorization: Bearer $TOKEN"
```

```json theme={null}
{
  "downloadUrl": "https://s3.amazonaws.com/bucket/archive.gz?X-Amz-Signature=...",
  "expiresAt": "2026-02-05T13:00:00Z",
  "checksum": "sha256:abc123def456..."
}
```

<Warning>The download handler confirms tenant ownership but does not check `COMPLETE`. It presigns the stored `archiveKey`. An archive receives that key and its checksum when it reaches `UPLOADED`, before `COMPLETE`. Treat `GET /v1/governance/archives/{id}/download` as a direct object-key path, not proof that archival completed. If you need only completed archives, select IDs from the list endpoint.</Warning>

<Warning>Archive availability depends on the configured S3-compatible storage backend and lifecycle policy. Confirm any restore requirement with the owner of that storage deployment before relying on a download URL.</Warning>

## Audit logs

***

Instrumented governance workflows write immutable, per-tenant audit records. Matcher links each record into a tamper-evident SHA-256 hash chain (`recordHash` = `SHA-256(prevHash || canonical content)`), so inconsistent changes are detectable. Use the verify endpoint below for the server-side integrity verdict.

### List audit logs

Cursor-paginated, with rich filters.

```bash theme={null}
curl -X GET "https://api.matcher.example.com/v1/governance/audit-logs?actor=user@example.com&action=CREATE&entity_type=context&date_from=2025-01-01&date_to=2025-01-31&limit=20" \
  -H "Authorization: Bearer $TOKEN"
```

```json theme={null}
{
  "items": [
    {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "tenantId": "550e8400-e29b-41d4-a716-446655440001",
      "entityType": "reconciliation_context",
      "entityId": "550e8400-e29b-41d4-a716-446655440002",
      "action": "CREATE",
      "actorId": "user@example.com",
      "changes": { },
      "truncated": false,
      "originalSize": 0,
      "createdAt": "2025-01-15T10:30:00Z",
      "tenantSeq": 1,
      "recordHash": "dd3f8a09dda3a8fdcd1e5c54ef76a9168bbabbfd92ad1dd736400d03a3f8a585",
      "prevHash": "0000000000000000000000000000000000000000000000000000000000000000",
      "hashVersion": 1
    }
  ],
  "limit": 20,
  "hasMore": false
}
```

Query parameters: `actor`, `action`, `entity_type`, `date_from`, `date_to` (`YYYY-MM-DD` or RFC 3339), `limit` (1–200, default 20), and `cursor`.

`actor` filters on the record's `actorId` value. That value is the raw actor identifier from the original write, an email address in this example. Audit records store the identifier as-is. It does not have to match an actor-mapping `actorId`.

When a diff exceeds the outbox payload cap, `changes` carries a truncation-marker envelope instead of the full diff, and `truncated` becomes `true` with `originalSize` reporting the pre-truncation byte size.

### Verify the audit chain

Re-verifies that every inspected record links to the previous one and matches its stored hash. Verification walks a contiguous span with a `maxRecords` bound, so `intact` speaks only for that inspected span. At the HTTP endpoint, a supplied `maxRecords` must be in the 1–10,000 range, and a value outside it returns `422`. When you omit it, Matcher uses the 10,000-record default.

`fromSeq` is an optional verification floor (minimum 1). Omit it to start at sequence 1. A floor above 1 trusts that record's stored `prevHash`, so the run does not cover tampering below the floor. The check stays strictly read-only. It detects tampering and never mutates a record.

```bash theme={null}
curl -X GET "https://api.matcher.example.com/v1/governance/audit-logs/verify?fromSeq=1&maxRecords=10000" \
  -H "Authorization: Bearer $TOKEN"
```

```json theme={null}
{
  "intact": true,
  "fromSeq": 1,
  "verifiedCount": 1024,
  "truncated": false
}
```

The `intact` field is `true` when the whole span stays unbroken. When the check finds a break, `firstBrokenSeq` reports the `tenantSeq` of the first failing record, and `verifiedCount` reports how many records held before it. The `truncated` field is `true` when the chain holds more records than the `maxRecords` inspection bound allowed. The response echoes the effective `fromSeq`. After a truncated run, resume with `fromSeq = fromSeq + verifiedCount` to inspect the next span.

### Get one audit log

```bash theme={null}
curl -X GET "https://api.matcher.example.com/v1/governance/audit-logs/{id}" \
  -H "Authorization: Bearer $TOKEN"
```

<Note>You can also list an entity's history directly with `GET /v1/governance/entities/{entityType}/{entityId}/audit-logs` (cursor-paginated), which is convenient when you already know the entity to audit.</Note>

## Response codes

***

| Status | Meaning                                                                                   |
| ------ | ----------------------------------------------------------------------------------------- |
| `200`  | Mapping, archive, or audit data returned                                                  |
| `204`  | Actor mapping pseudonymized or deleted                                                    |
| `400`  | Application-level invalid input (missing displayName/email or an invalid date)            |
| `403`  | Missing the required permission tier (e.g. `deanonymize` for single-record PII)           |
| `404`  | Actor mapping, archive, or audit log not found                                            |
| `422`  | Request/schema validation failed (e.g. invalid email format or a constrained query value) |
