# API reference (/docs/reference/api)

> Every method of the Better IAM server API: 64 groups and 767 methods, what each does, its HTTP route, and its TypeScript signature.



`betterIam(options)` returns one object that is your whole identity server. You create it once, usually in
`lib/iam.ts`, export it as `iam`, and import it wherever server code needs to check access, serve the IAM routes, or
run maintenance. This reference covers all of it: the `api` groups that manage organizations, people, and access
(listed under Groups), and the functions on the instance itself (listed under Instance functions).

| Member                                                                                                                                                                                                                                             | What it is for                                                                                                                                                                                                                                                                                                              |
| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `api`                                                                                                                                                                                                                                              | Every provisioning and authentication method, by group: `iam.api.groups.addMember(credential, input)`. The HTTP routes and the browser client call the same methods.                                                                                                                                                        |
| `authorize`, `authorizeMany`, `listAccessible`, `require`, `authenticate`                                                                                                                                                                          | The checks your own routes, pages, and jobs run before they touch product data.                                                                                                                                                                                                                                             |
| `callPlugin`                                                                                                                                                                                                                                       | Calls a plugin endpoint through the same authorized, audited envelope as built-in methods.                                                                                                                                                                                                                                  |
| `handler`, `nodeHandler`                                                                                                                                                                                                                           | The HTTP transports. Mount one of them at `basePath` (default `/api/iam`); see [HTTP handlers](#http-handlers).                                                                                                                                                                                                             |
| `events`                                                                                                                                                                                                                                           | `subscribe(patterns, handler)` for in-process audit subscribers, and `dispatch()` to run them (the same function as `dispatchAuditHooks`).                                                                                                                                                                                  |
| `useProtocol`, `protocolHost`                                                                                                                                                                                                                      | Mount OAuth, SAML, and SCIM services, and the host callbacks those packages are built from.                                                                                                                                                                                                                                 |
| `initialize`, `bootstrap`, `recoverRoot`, `rotateSecrets`, `selfCheck`                                                                                                                                                                             | Deployment operations: schema, the first administrator, break-glass recovery, secret rotation, and health findings.                                                                                                                                                                                                         |
| `purgeDeleted`, `sweepExpired`, `reconcilePackages`, `sendAccessDigest`, `sendExpiryReminders`, `closeOverdueCertifications`, `closeOverdueTeamReviews`, `checkInvariants`, `archiveAudit`, `pruneAudit`, `dispatchAuditHooks`, `flushAccessUsage` | Scheduled jobs and shutdown work. See [Scheduled jobs](/docs/operations/jobs).                                                                                                                                                                                                                                              |
| `signals`                                                                                                                                                                                                                                          | The Shared Signals receiver's server side: the poll job `signals.poll()` and `signals.receive(sourceId, set)` for custom transports. See [Shared Signals receiver](#shared-signals-receiver).                                                                                                                               |
| `licenses`                                                                                                                                                                                                                                         | License management's server side: the jobs `licenses.reconcile()` and `licenses.reclaim()`, and `licenses.features(identityId, tenantId, { trustedTenantId? })` (platform products unless you trust a tenant) and `licenses.products(...)` for application code. See [License jobs and helpers](#license-jobs-and-helpers). |
| `guests`                                                                                                                                                                                                                                           | Guest collaboration's server side: the jobs `guests.sweep()` and `guests.sendReviewReminders()`. See [Guest collaboration jobs](#guest-collaboration-jobs).                                                                                                                                                                 |
| `clearances`                                                                                                                                                                                                                                       | Security clearances' server side (with the `clearances` option): the job `clearances.sendReminders()`. See [Security clearance jobs](#security-clearance-jobs).                                                                                                                                                             |
| `assertionKey`, `assertionKeys`                                                                                                                                                                                                                    | The keys downstream services verify [assertions](/docs/reference/api/assertions#issue) with.                                                                                                                                                                                                                                |
| `auth`                                                                                                                                                                                                                                             | Low-level authentication primitives for trusted integrations, such as `dispatchOutbox()` and `withClient()`.                                                                                                                                                                                                                |
| `store`                                                                                                                                                                                                                                            | The storage adapter you passed as `database`.                                                                                                                                                                                                                                                                               |
| `metrics`, `sessionTokens`, `endpoint`                                                                                                                                                                                                             | Prometheus-style metrics (with `observability.metrics`), session JWT keys and online verification (with `sts.jwt`), and where the handler is mounted (origin, `basePath`, cookie settings) for framework integrations.                                                                                                      |

Keep the instance on the server. `auth`, `store`, `bootstrap`, `recoverRoot`, `assertionKey`, and `protocolHost` act
with the deployment's authority rather than a caller's permissions. The HTTP router leaves them out on purpose, and you
should never expose them through RPC reflection or send them to a browser.

## Three ways to call it [#three-ways-to-call-it]

Every group method is called three ways: on the server as `iam.api.{group}.{method}(credential, input)`, from the typed browser client as `client.{group}.{method}(input)`, and over HTTP as `POST {basePath}/{group}/{method}` with a JSON body. The default `basePath` is `/api/iam`. The first server argument, the credential, says who is calling: a session token, an API key, or the incoming request headers (which carry the session cookie or an `Authorization: Bearer` token).

Over HTTP every call is a `POST` with `Content-Type: application/json` and the `X-Better-IAM: 1` header (anything else is refused with `CSRF_REJECTED`). Requests that carry cookies must also send an exact trusted `Origin`. Responses use a `{ "data": … }` or `{ "error": { "code", "message" } }` envelope; the browser client unwraps it for you. Root bootstrap, recovery, raw storage, and session-issuance primitives are never exposed over HTTP.

> **OpenAPI 3.1 specification.** 
  The HTTP API is also described as an [OpenAPI 3.1 document](/openapi.json), generated from the same TypeScript types as this reference, with JSON Schemas for every request and response. Import it into Postman, Insomnia, or Bruno, or feed it to a client generator for languages other than TypeScript.

  **Server:**

    ```ts
    import { iam } from './iam';

    // The first argument is the caller: a session token, an API key, or request headers.
    const credential = { headers: request.headers };
    const group = await iam.api.groups.create(credential, { tenantId, name: 'Finance' });
    ```
  
  **Browser client:**

    ```ts
    import { createIamClient } from 'better-iam/client';
    import type { iam } from './iam';

    const client = createIamClient<typeof iam>({ baseURL: 'https://identity.example.com' });
    const group = await client.groups.create({ tenantId, name: 'Finance' }); // the session cookie is the credential
    ```
  
  **HTTP:**

    ```bash
    curl -X POST https://identity.example.com/api/iam/groups/create \
      -H "Authorization: Bearer $BETTER_IAM_TOKEN" \
      -H "Content-Type: application/json" \
      -H "X-Better-IAM: 1" \
      -d '{ "tenantId": "ten_…", "name": "Finance" }'
    ```
  
## HTTP handlers [#http-handlers]

`iam.handler(request)` serves the Fetch API (a `Request` in, a `Response` out) and `iam.nodeHandler(req, res)` serves
Node's `http` interface. They route identically, so mount exactly one of them. Both need Node.js 22.12 or later; edge
runtimes are not supported. They answer:

* `POST {basePath}/{group}/{method}` for every routed group method, plus `POST {basePath}/authorize`,
  `/authorizeMany`, `/listAccessible`, and `/plugins/{pluginId}/{path}` for plugin endpoints;
* `GET {basePath}/health` (one database read, status 503 when storage fails), `GET {basePath}/metrics` (only with
  `observability.metrics.bearerToken`), and `GET {basePath}/.well-known/jwks.json` (only with `sts.jwt`);
* the routes of every protocol service mounted with [`useProtocol`](#useprotocol), which are asked first;
* `POST {signals.pushPath}/{sourceId}`, the [Shared Signals push endpoint](#shared-signals-receiver), also asked
  before the API routes.

`nodeHandler` is required when you mount the OAuth authorization server, which only speaks Node's request and
response objects. It offers each request to the Node protocol mounts first and hands everything else to `handler`.

Both enforce the browser boundary: JSON bodies with `X-Better-IAM: 1`, an exact trusted `Origin` for requests that
carry cookies (`CSRF_REJECTED` or `UNTRUSTED_ORIGIN` otherwise), and API bodies of at most 64 KiB
(`PAYLOAD_TOO_LARGE`). They set the session cookie on sign-in routes and clear it on sign-out, add CORS headers for
trusted origins, echo a plain `X-Request-Id`, and return every error as `{ "error": { "code", "message" } }`. The
framework packages ([Next.js](/docs/integrations/nextjs), [NestJS](/docs/integrations/nestjs),
[Node middleware](/docs/integrations/node)) wrap them for you.

```ts
import { createServer } from 'node:http';
import { iam } from './lib/iam';

// A dedicated identity server: IAM routes, health, metrics, and every mounted protocol.
createServer((req, res) => iam.nodeHandler(req, res)).listen(3000);

// Or, inside a Fetch-style router running on Node (Hono shown):
app.all('/api/iam/*', (c) => iam.handler(c.req.raw));
```

## When to call each function [#when-to-call-each-function]

| When                            | Functions                                                                                                                                                                                                                                                                                                                                                                           |
| ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| In the request path             | `authenticate`, `authorize`, `require`, `authorizeMany`, `listAccessible`, `callPlugin`                                                                                                                                                                                                                                                                                             |
| Once at process start           | `useProtocol`, `iam.events.subscribe`                                                                                                                                                                                                                                                                                                                                               |
| Once per deploy or installation | `initialize` (CLI `migrate`), `bootstrap`, `selfCheck` (CLI `doctor`)                                                                                                                                                                                                                                                                                                               |
| On a schedule                   | `dispatchAuditHooks`, `purgeDeleted`, `sweepExpired`, `reconcilePackages`, `sendAccessDigest`, `sendExpiryReminders`, `closeOverdueCertifications`, `closeOverdueTeamReviews`, `checkInvariants`, `detectThreats`, `signals.poll`, `licenses.reconcile`, `licenses.reclaim`, `guests.sweep`, `guests.sendReviewReminders`, `clearances.sendReminders`, `archiveAudit`, `pruneAudit` |
| Rarely, by an operator          | `recoverRoot`, `rotateSecrets`, `assertionKey`, `assertionKeys`                                                                                                                                                                                                                                                                                                                     |
| At shutdown                     | `flushAccessUsage`                                                                                                                                                                                                                                                                                                                                                                  |

Deployment operations and jobs take no credential. They act as `deployment-operator`, the actor you see on the audit
events they record, so protect the process and configuration that runs them like a root credential. Every job is safe
to run again and to overlap with itself; each function below says how it avoids doing work twice, and
[`selfCheck`](#selfcheck) reports the jobs that have stopped running. Most jobs have a matching
[CLI command](/docs/reference/cli) for cron.

## Shared Signals receiver [#shared-signals-receiver]

`iam.signals` is the server side of the Shared Signals receiver: security events (CAEP and RISC SETs) that an
organization's identity providers send about its people. Sources are registered and events read through the
[signals group](/docs/reference/api/signals). Push sources need nothing scheduled: providers POST to
`{signals.pushPath}/{sourceId}` (`/api/iam/signals/push/{sourceId}` by default), which `handler` and `nodeHandler`
serve ahead of the API routes. The `signals` option sets `pushPath`, and `allowPrivateNetworks` and
`allowInsecureLocalhost` for the address rules of key, discovery, and poll requests.

### signals.poll [#signalspoll]

Fetches events from every active poll source (RFC 8936), or from one with `sourceId`.

* **When:** on a schedule, every minute or so, when any organization has a poll source. The interval is how late a
  polled event can be.
* **Permission:** none. A deployment operation; [`signals.poll`](/docs/reference/api/signals#poll) is the per-source
  call for administrators.
* **Audited as:** `signal:received` for each new event and `signal:revoke-sessions` for sessions it ends, by the actor
  `signal:{sourceId}`.
* **Errors:** `INVALID_INPUT` for a malformed `sourceId`. A failing provider never throws: it is counted in `errors`
  and recorded on the source as `lastError`.
* **Safe to repeat:** yes. Events are deduplicated on issuer and `jti`, so an event delivered again, for example after
  overlapping runs, is recognized and acknowledged again.

Each source is asked with `returnImmediately: true` and up to `maxEvents` events per request, following
`moreAvailable` for at most five requests. Every SET is verified and recorded as a push would be. Stored events are
acknowledged with the next request, refused ones are reported in `setErrs`, and events that cannot be handled yet (keys
unavailable, storage busy) are left for the provider to deliver again. Sources of suspended organizations are skipped.
The result is `{ sources, received, acknowledged, errors }`.

```ts
setInterval(() => void iam.signals.poll().catch(reportError), 60_000).unref();
```

### signals.receive [#signalsreceive]

Verifies and records one SET for a source, exactly as the push endpoint does, for transports of your own.

* **When:** in the code that takes SETs from a queue, a bridge, or tests.
* **Permission:** none. Trusted server code; the SET's signature is what authenticates it.
* **Audited as:** `signal:received`, and `signal:revoke-sessions` when the source's action ends sessions.
* **Errors:** `SIGNAL_REJECTED` (a `SignalRejectedError` with the RFC 8935 `err`, and `temporary: true` when a later
  attempt may pass because the source's keys could not be fetched); `NOT_FOUND` for an unknown or disabled source, or
  one whose organization is suspended.
* **Safe to repeat:** yes. A SET received before returns its first receipt with `duplicate: true`.

The result is `{ eventId, status, duplicate, identityId? }`, where `eventId` is the received event for
[`signals.getEvent`](/docs/reference/api/signals#getevent).

```ts
const receipt = await iam.signals.receive(sourceId, compactSet);
if (receipt.status === 'unmatched') console.warn('No person matched', receipt.eventId);
```

## License jobs and helpers [#license-jobs-and-helpers]

`iam.licenses` is the server side of [license management](/docs/reference/api/licenses): two scheduler jobs that keep
seats in line with time and activity, and two credential-free readers for application code. Products, pools,
assignments, and seats are managed through the licenses group. Every seat change the jobs make is audited as
`license:seat-activate`, `license:seat-waiting`, or `license:seat-release` by `deployment-operator`, and birthright
package rules on `identity.licenses` are re-evaluated for the people whose seats changed.

### licenses.reconcile [#licensesreconcile]

Brings every active organization's seats in line with assignments, memberships, identity status, and pool terms.

* **When:** on a schedule, hourly. The interval is how late a pool that starts or ends, or a membership or account
  that expires, is reflected in seats.
* **Permission:** none. A deployment operation.
* **Audited as:** the seat changes, with reason `schedule`.
* **Errors:** `NOT_FOUND` for an unknown `tenantId`. A tenant that fails does not throw: it is listed in
  `failedTenants` and the others carry on.
* **Safe to repeat:** yes. Reconciliation is idempotent, and each tenant with licenses is reconciled in its own
  transaction; a second run over unchanged data changes nothing.

Everything else already moves seats as it happens (assignments, pool changes, group membership, disabling, deletion),
so this job catches what changes with time and repairs anything edited by hand. Until it runs nothing is granted past
its time: decisions stop counting a seat held only through a lapsed membership, and an expired account holds nothing.
Suspended tenants are skipped, and a retired product never gets seats back. The result is
`{ tenants, activated, waiting, released, failedTenants }`, where `tenants` counts the tenants with licenses.

```ts
setInterval(() => void iam.licenses.reconcile().catch(reportError), 60 * 60 * 1000).unref();
```

### licenses.reclaim [#licensesreclaim]

Removes the direct license assignments of people who have not used their account for the tenant's `reclaimAfterDays`.

* **When:** on a schedule, daily.
* **Permission:** none. A deployment operation; tenants opt in with
  [`licenses.configure`](/docs/reference/api/licenses#configure).
* **Audited as:** `license:reclaim` on the person for each removed assignment (metadata: `productId`, `productKey`,
  `assignedAt`, `lastActivityAt`, `reclaimAfterDays`), then the seat changes with reason `reclaim`.
* **Errors:** `INVALID_INPUT` for a malformed `tenantId`. A tenant that fails, including an unknown `tenantId`, does
  not throw: it is listed in `failedTenants`.
* **Safe to repeat:** yes. Each tenant runs in its own transaction, and a removed assignment is gone, so a second pass
  finds nothing more.

It visits the tenants with `reclaimAfterDays` set (or only `tenantId`). A person is inactive when their last sign-in
(or, without one, their account's creation) and the last use of their own sessions and API keys are older than the
period; "view as" sessions of administrators do not count. The job removes each direct assignment older than the period
whose holder is inactive, and waiting people take the freed seats. Seats held through groups are never reclaimed
(`licenses.usage` reports them as `reclaimableThroughGroups`). Each pass also stores the activity it saw on every seat
(`lastActivityAt`). The result is `{ tenants, reclaimed, failedTenants }`, with the `tenantId`, `productId`,
`productKey`, `identityId`, and `lastActivityAt` of every reclaimed assignment.

```ts
const { reclaimed } = await iam.licenses.reclaim();
for (const item of reclaimed) console.info('Reclaimed', item.productKey, 'from', item.identityId);
```

### licenses.features [#licensesfeatures]

Returns the feature keys a person's active seats unlock in a tenant, for server code without a credential.

* **When:** in your own routes and jobs, wherever a feature depends on the person's license.
* **Permission:** none. Trusted server code.
* **Audited as:** not audited.
* **Errors:** `NOT_FOUND` for an unknown tenant or `trustedTenantId`; `INVALID_INPUT` for a malformed `identityId`
  or `trustedTenantId`.

The result is the sorted union of the `featureKeys` of the products the person holds an active seat for, counting
only products defined by the tenant you trust or a tenant above it: the root tenant (the platform's products) unless
you pass `{ trustedTenantId }`. Any tenant can define a product of its own listing any feature key, so the default
keeps a tenant from minting a paid feature for itself; pass an organization's id to also count the products it sells to
its own projects. People who are not active, whose account expired, or who belong to another tenant get an empty list,
and a seat held only through a lapsed membership does not count. Tenant feature flags are separate: combine the two
when a flag rolls a feature out and the license entitles the person.

```ts
const features = await iam.licenses.features(identityId, tenantId);
if ((await iam.features.isEnabled(tenantId, 'exports')) && features.includes('exports')) showExport();
// Also count what Acme sells to its own projects:
const acme = await iam.licenses.features(identityId, projectId, { trustedTenantId: acmeId });
```

### licenses.products [#licensesproducts]

Returns the keys of the products a person holds an active seat for in a tenant: exactly what policies see as `principal.licenses`.

* **When:** in server code that needs the person's products rather than their feature keys.
* **Permission:** none. Trusted server code.
* **Audited as:** not audited.
* **Errors:** as for `licenses.features`.

The same people get an empty list as for `licenses.features` above. Retired products and waiting seats never appear.

## Guest collaboration jobs [#guest-collaboration-jobs]

`iam.guests` is the server side of [guest collaboration](/docs/reference/api/guests): two scheduler jobs that keep
invitations, guest accounts, and sponsors in order. Guests, invitations, and cross-tenant access settings are managed
through the guests group. Identity expiry itself is the retention worker's: [`purgeDeleted`](#purgedeleted) disables a
guest whose access ended, and [`sweepExpired`](#sweepexpired) deletes settled invitations 90 days after they lapsed.

### guests.sweep [#guestssweep]

Marks lapsed guest invitations and ended guest accounts expired, and flags guests whose sponsor can no longer sponsor.

* **When:** on a schedule, hourly, before `guests.sendReviewReminders` and an outbox run.
* **Permission:** none. A deployment operation.
* **Audited as:** `guest:expire` for each guest account it closes, `guest:sponsor-missing` for each guest it flags,
  and `guest:settings` (with `partnersRemoved`) when it drops partner entries, all by `deployment-operator`.
* **Errors:** `INVALID_INPUT` for a malformed `tenantId`.
* **Safe to repeat:** yes. Each tenant with pending invitations, open guest accounts, or settings runs in its own
  transaction, and every change is a state that a second run finds already made.

For every such tenant (or only `tenantId`) it marks pending invitations past their lapse `expired`, closes the guest
account of each guest whose access ended (and reopens one an administrator restored with a later end and a
re-enabled identity), removes the accounts of deleted identities, and flags `sponsorMissing` on active guests whose
sponsor is disabled, expired, deleted, or otherwise unable to sponsor. When the deployment sends email and the tenant
is active, the tenant's active owners get one `guest-sponsor-missing` email per guest and loss; the flag clears once
the sponsor can sponsor again or [`guests.setSponsor`](/docs/reference/api/guests#setsponsor) names a new one. It also
drops partner entries that name purged tenants. The result is
`{ invitationsExpired, accountsExpired, accountsRestored, sponsorsMissing, ownersNotified, partnersRemoved }`.

### guests.sendReviewReminders [#guestssendreviewreminders]

Emails each sponsor about the guests whose review is due or whose access ends soon.

* **When:** on a schedule, hourly, followed by an outbox run.
* **Permission:** none. A deployment operation.
* **Audited as:** `guest:review-reminder` on each guest reminded, by `deployment-operator`, with the sponsor, the
  earliest date reminded, and the access end.
* **Errors:** `DELIVERY_REQUIRED` when `authentication.sendEmail` is not configured; `INVALID_INPUT` for a
  `withinDays` outside 1 to 90 or a malformed `tenantId`.
* **Safe to repeat:** yes. A reminder mark per guest and date (`guest-review:{id}:{reviewDueAt}`,
  `guest-end:{id}:{expiresAt}`) prevents repeats, and each tenant runs in its own transaction.

It looks at the active guests of every active tenant (or only `tenantId`) and reminds the sponsor of each review date
and each access end within `withinDays` (default 14) once, in one `guest-review` email per guest that covers whatever
is due, so an unanswered review reminder never stands in for the warning that access is about to end. A renewal moves
both dates, which brings fresh reminders when they come near again. Guests whose sponsor cannot sponsor or has no email
address are skipped (the sweep reports them to the owners). The result is `{ sent, skipped: { inactive, quiet } }`,
where each `sent` entry names the tenant, guest, sponsor, and `dueAt`.

```ts
setInterval(async () => {
  await iam.guests.sweep();
  await iam.guests.sendReviewReminders();
  await iam.auth.dispatchOutbox();
}, 60 * 60 * 1000).unref();
```

## Security clearance jobs [#security-clearance-jobs]

`iam.clearances` is the server side of [security clearances](/docs/reference/api/clearances) (the `clearances`
option). Schemes, clearances, and labels are managed through the clearances group, and decisions enforce labels by
themselves: suspensions, expiries, interim policy, and NDA acceptance are evaluated on every decision, so no job has to
run for them to take effect.

### clearances.sendReminders [#clearancessendreminders]

Emails security officers about clearances whose reinvestigation is due or whose end comes up.

* **When:** on a schedule, daily, followed by an outbox run.
* **Permission:** none. A deployment operation.
* **Audited as:** `clearance:reminder` on each person reminded, by `deployment-operator`, with the level id, the
  earliest date reminded (`dueAt`), the `kinds` (`reinvestigation`, `interim-end`, `expiry`), and how many addresses
  were emailed.
* **Errors:** `FEATURE_DISABLED` (403) without the `clearances` option; `DELIVERY_REQUIRED` when
  `authentication.sendEmail` is not configured; `INVALID_INPUT` for a `withinDays` outside 1 to 365 or a malformed
  `tenantId`.
* **Safe to repeat:** yes. A reminder mark per clearance and date (`clearance-reminder:{id}:{date}`) prevents repeats,
  and each tenant runs in its own transaction.

It looks at the active and interim clearances of every active tenant that holds any (or only those held in
`tenantId`) and reminds once of each periodic reinvestigation that is due or overdue and each end, interim or final,
within `withinDays` (default 60). One `clearance-reminder` email per clearance goes to every active owner of the tenant
that defines the scheme (a project's clearances to the organization's owners) and to the scheme's `notify.emails`. The
email names the person and the level, never compartments, and links to `links.clearances` (or `links.account`). A
changed date is reminded again. People who are not active are skipped. The result is
`{ sent, skipped: { inactive, noRecipients } }`, where each `sent` entry names the tenant, the person, `dueAt`, and how
many addresses were emailed, `inactive` counts tenants that are not active (an unknown `tenantId` included), and
`noRecipients` clearances with something due but nobody to tell.

```ts
setInterval(async () => {
  await iam.clearances.sendReminders();
  await iam.auth.dispatchOutbox();
}, 24 * 60 * 60 * 1000).unref();
```

## Groups [#groups]

### Sign-in and people [#sign-in-and-people]

Authenticate people and machines, manage who exists in a tenant, and how they are organized.

  - [auth](/docs/reference/api/auth): End-user authentication: sign-in methods, MFA, passkeys, sessions, devices, and account recovery. (40 methods)

  - [identities](/docs/reference/api/identities): People in a tenant: invitations, profiles, attributes, sessions, offboarding, and data-subject export. (24 methods)

  - [guests](/docs/reference/api/guests): B2B guests: email invitations, sponsors who renew access, cross-tenant access settings, and a guest ceiling. (14 methods)

  - [teams](/docs/reference/api/teams): Nested teams with maintainers, join requests, and access through a team-managed group. (27 methods)

  - [departments](/docs/reference/api/departments): The org chart: departments with heads, one department per person, import from attributes, heads as managers. (14 methods)

  - [serviceAccounts](/docs/reference/api/service-accounts): Service accounts for machines, with scheduled deactivation. (6 methods)

  - [credentials](/docs/reference/api/credentials): API keys with scopes, labels, expiry, and last-use tracking. (6 methods)

  - [links](/docs/reference/api/links): Explicit account linking between identities, and switching between linked accounts. (4 methods)

  - [domains](/docs/reference/api/domains): Verified email domains for discovery and single sign-on routing. (5 methods)

  - [security](/docs/reference/api/security): Network blocks for addresses and ranges. (3 methods)

  - [devices](/docs/reference/api/devices): Device posture: registered devices, session-bound device proofs, MDM and EDR compliance, and enrollment codes. (20 methods)

### Organizations [#organizations]

The tenant tree, organization addresses and settings, and platform administration.

  - [tenants](/docs/reference/api/tenants): Tenant trees: organizations, settings, authentication and access policies, limits, and usage. (19 methods)

  - [hostnames](/docs/reference/api/hostnames): Custom hostnames organizations verify with DNS and use as their own sign-in address. (5 methods)

  - [features](/docs/reference/api/features): Feature flags at platform and organization level: definitions, targets, overrides, and evaluation. (8 methods)

  - [onboarding](/docs/reference/api/onboarding): Onboarding checklists for newcomers and new tenants, customized at platform, organization, and project level. (15 methods)

  - [root](/docs/reference/api/root): Platform root administration. (2 methods)

  - [trust](/docs/reference/api/trust): Cross-tenant trust for platform-controlled role assumption. (6 methods)

  - [sts](/docs/reference/api/sts): Temporary security credentials: short-lived sessions for assumed roles, with a duration, session policies, and tags. (3 methods)

  - [oidcProviders](/docs/reference/api/oidc-providers): Trusted external OpenID Connect providers whose tokens can be exchanged for temporary credentials. (6 methods)

### Access model [#access-model]

What can be done to what, and who holds which permissions.

  - [roles](/docs/reference/api/roles): Custom roles built from permissions or conditional policies, inheritance, and role assumption. (9 methods)

  - [policies](/docs/reference/api/policies): Versioned JSON policies, candidate testing, simulation, and who-can / what-can reviews. (11 methods)

  - [bindings](/docs/reference/api/bindings): Role bindings for identities and groups, including temporary, future-dated, and just-in-time eligible bindings. (12 methods)

  - [groups](/docs/reference/api/groups): Groups and (optionally temporary) group memberships. (10 methods)

  - [authorities](/docs/reference/api/authorities): Delegated grant authorities: who may hand out which roles below them. (2 methods)

  - [resourceTypes](/docs/reference/api/resource-types): Tenant-defined resource types with actions, typed attributes, and relations. (5 methods)

  - [actions](/docs/reference/api/actions): Tenant-defined actions added to the permission catalog. (3 methods)

  - [resources](/docs/reference/api/resources): IAM-managed resources with owners and parents. (6 methods)

  - [relationships](/docs/reference/api/relationships): Relationship tuples (ReBAC) that policies read as `resource.relations` . (3 methods)

  - [filters](/docs/reference/api/filters): Data filtering: which resources of a type the caller may act on, as a filter for your database query. (2 methods)

  - [clearances](/docs/reference/api/clearances): Security clearances: classification schemes, adjudicated clearances and compartments, and labels every decision enforces. (19 methods)

### Access lifecycle [#access-lifecycle]

Granting access for a purpose and a time, and reporting on it.

  - [packages](/docs/reference/api/packages): Access packages: bundles of roles and memberships assigned, requested, or granted by rule. (18 methods)

  - [accessRequests](/docs/reference/api/access-requests): Time-boxed access requests that reviewers approve under their own authority. (7 methods)

  - [accessPaths](/docs/reference/api/access-paths): Self-service answers to "how can I get access?": the requests, packages, and elevations that would grant an action. (1 methods)

  - [reports](/docs/reference/api/reports): The access report: what ends soon, unused keys, live activations, and pending requests. (1 methods)

  - [config](/docs/reference/api/config): Configuration as code: export, plan, and apply a tenant’s access model by name. (3 methods)

  - [workflows](/docs/reference/api/workflows): Lifecycle workflows: joiner, mover, and leaver automation with triggers, scopes, steps, and a daily brake. (12 methods)

  - [applications](/docs/reference/api/applications): The application catalog behind My apps: apps assigned to people and groups, requests, launches, and usage. (12 methods)

### Governance [#governance]

Reviewing, analyzing, and constraining access over time.

  - [certifications](/docs/reference/api/certifications): Access certification campaigns: reviewers keep or revoke access with evidence. (9 methods)

  - [analysis](/docs/reference/api/analysis): Access-analysis findings (dormant access, stale keys, policy lint, separation of duties) and suppressions. (4 methods)

  - [roleMining](/docs/reference/api/role-mining): Role mining: bundle suggestions, right-sizing, peer outliers, and usage. (6 methods)

  - [sod](/docs/reference/api/sod): Separation-of-duties rules enforced when access is granted. (5 methods)

  - [invariants](/docs/reference/api/invariants): Access invariants: guardrails that must hold whatever roles and policies say. (5 methods)

  - [impact](/docs/reference/api/impact): Change-impact previews: who gains and loses access before a role, policy, or binding changes. (1 methods)

  - [agreements](/docs/reference/api/agreements): Versioned terms of use that members accept and policies can require. (7 methods)

  - [compliance](/docs/reference/api/compliance): The compliance center: automated checks mapped to SOC 2, ISO 27001, NIST 800-53, and GDPR, with signed evidence. (17 methods)

### Events and integrations [#events-and-integrations]

The audit log, outgoing events, and service-to-service trust.

  - [audit](/docs/reference/api/audit): The tamper-evident audit log: search, chain verification, and JSON Lines export. (3 methods)

  - [webhooks](/docs/reference/api/webhooks): Signed webhook subscriptions, filters, delivery history, and redelivery. (9 methods)

  - [assertions](/docs/reference/api/assertions): Short-lived signed assertions for calling downstream services without sharing sessions. (1 methods)

### AI agents and models [#ai-agents-and-models]

Agents as accounts, what they may do for people, and governed access to models.

  - [agents](/docs/reference/api/agents): AI agents as accounts: sponsors, ceilings, and a kill switch. (14 methods)

  - [delegations](/docs/reference/api/delegations): Agents acting on a person's behalf, within a scope and for a time. (16 methods)

  - [inference](/docs/reference/api/inference): Model access, provider keys, budgets, and metering. (16 methods)

### Billing and usage [#billing-and-usage]

What people, teams, and organizations spend, the budgets and statements around it, API usage plans, and license seats.

  - [billing](/docs/reference/api/billing): Spend tracking for people, teams, and organizations: meters, prices, usage, budgets, credits, and statements. (62 methods)

  - [quotas](/docs/reference/api/quotas): API usage plans: throttles and quotas per API key, agent, person, group, or organization. (12 methods)

  - [licenses](/docs/reference/api/licenses): License management: products, pools of seats, assignments to people and groups, and seats with a waiting list. (18 methods)

### Secrets, keys, and certificates [#secrets-keys-and-certificates]

Cryptographic keys, secrets, tokenized data, and certificates for people, workloads, and servers.

  - [keys](/docs/reference/api/keys): Key management: tenant keys for encryption, signatures, MACs, and JWTs whose material never leaves the server. (30 methods)

  - [vault](/docs/reference/api/vault): The secrets vault: versioned secrets, rotation, check-outs of shared credentials, and dynamic leases. (23 methods)

  - [protection](/docs/reference/api/protection): Data protection by tokenization: profiles, tokens, masks, and purpose-bound detokenization. (9 methods)

  - [pki](/docs/reference/api/pki): The private certificate authority: X.509 roots and intermediates, certificates, SPIFFE SVIDs, and revocation. (11 methods)

  - [ssh](/docs/reference/api/ssh): SSH access: short-lived OpenSSH certificates decided by policy, host enrollment, and revocation lists. (30 methods)

### Directory and credentials [#directory-and-credentials]

The directory served to LDAP applications, and credentials people carry in their wallets.

  - [ldap](/docs/reference/api/ldap): The LDAP directory gateway: publish an organization directory to applications that only speak LDAP. (3 methods)

  - [verifiableCredentials](/docs/reference/api/verifiable-credentials): Verifiable credentials: SD-JWT VCs issued to wallets over OpenID4VCI, status lists, and verification. (20 methods)

### Privacy [#privacy]

Consent to processing purposes, data-subject requests with statutory deadlines, and legal holds.

  - [privacy](/docs/reference/api/privacy): Privacy and consent: purposes, consent receipts, data-subject requests with deadlines, and legal holds. (39 methods)

### Threat detection and response [#threat-detection-and-response]

Attacks on identities found in the audit trail or reported by upstream identity providers, the incidents and risk they raise, and the responses to them.

  - [threats](/docs/reference/api/threats): Identity threat detection and response: detection rules, incidents, identity risk, responses, and playbooks. (24 methods)

  - [signals](/docs/reference/api/signals): The Shared Signals receiver: CAEP and RISC security events from upstream identity providers, by push or poll. (10 methods)

## Instance functions [#instance-functions]

Functions on the object `betterIam()` returns, outside the `api` groups. Checks such as `authorize` and `require` are what your routes call; deployment operations (migrations, scheduled jobs, secret rotation) have no HTTP routes, so you run them from a worker, a cron job, or the [CLI](/docs/reference/cli).

| Function                                                    | What it does                                                                                                                          |
| ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| [`archiveAudit`](#archiveaudit)                             | Copies every tenant's new audit events, verified and in chain order, to the configured `auditArchive` sink.                           |
| [`assertionKey`](#assertionkey)                             | Returns the key downstream services use to verify the assertions this deployment issues.                                              |
| [`assertionKeys`](#assertionkeys)                           | Returns the assertion key of the current secret followed by those of `previousSecrets`, for verifiers during a secret rotation.       |
| [`authenticate`](#authenticate)                             | Resolves a credential to the identity and session behind it, re-checking everything that could have revoked it.                       |
| [`authorize`](#authorize)                                   | Decides whether the caller may perform one action on one resource and returns the decision instead of throwing.                       |
| [`authorizeMany`](#authorizemany)                           | Evaluates up to 50 checks for one caller in one tenant in a single transaction, for rendering UI state.                               |
| [`bootstrap`](#bootstrap)                                   | Creates the platform's root tenant and its first root administrator, once per installation.                                           |
| [`callPlugin`](#callplugin)                                 | Runs a plugin endpoint from server code inside the same authorized, audited transaction as a built-in method.                         |
| [`checkInvariants`](#checkinvariants)                       | Evaluates the access invariants of every active organization and records an audit event when one breaks or recovers.                  |
| [`closeOverdueCertifications`](#closeoverduecertifications) | Closes every auto-closing certification campaign whose due date has passed and applies its decisions.                                 |
| [`closeOverdueTeamReviews`](#closeoverdueteamreviews)       | Completes every open team membership review whose due date has passed and removes the people it decided to remove.                    |
| [`detectThreats`](#detectthreats)                           | Reads each active organization's new audit events, verifies their hash chain, and raises threat detections, incidents, and responses. |
| [`dispatchAuditHooks`](#dispatchaudithooks)                 | Runs plugin `afterAudit` hooks, `events.onEvent`, and in-process subscribers for audit events that have committed.                    |
| [`flushAccessUsage`](#flushaccessusage)                     | Writes the access usage buffered in memory to storage now.                                                                            |
| [`initialize`](#initialize)                                 | Creates or upgrades the database schema and runs one-time data upgrades so the instance can serve requests.                           |
| [`listAccessible`](#listaccessible)                         | Lists the registered resources of one managed type that the caller may perform an action on, one page at a time.                      |
| [`planResources`](#planresources)                           | Returns which resources of one type the caller may perform an action on, as a filter for your own database query.                     |
| [`pruneAudit`](#pruneaudit)                                 | Deletes one tenant's audit events older than a retention period while keeping the rest of the chain verifiable.                       |
| [`purgeDeleted`](#purgedeleted)                             | Runs the retention worker: ends access whose time is up and removes deleted tenants past their retention window.                      |
| [`reconcilePackages`](#reconcilepackages)                   | Applies access-package rules: people who match a rule receive the package, and automatic holders who stopped matching lose it.        |
| [`recoverRoot`](#recoverroot)                               | Creates an additional root administrator in the root tenant when nobody can sign in as root any more.                                 |
| [`require`](#require)                                       | Checks one action like `authorize` and throws `ACCESS_DENIED` (403) when it is not allowed.                                           |
| [`rotateSecrets`](#rotatesecrets)                           | Re-seals values encrypted with a previous deployment secret using the current one, so the old secret can be retired.                  |
| [`selfCheck`](#selfcheck)                                   | Reports configuration and storage problems an operator should act on, each with a severity and a fix.                                 |
| [`sendAccessDigest`](#sendaccessdigest)                     | Emails each organization's owners its access report when the report has something in it, at most once per interval.                   |
| [`sendExpiryReminders`](#sendexpiryreminders)               | Emails each person whose access ends soon one reminder listing it, once per item and end date.                                        |
| [`sweepExpired`](#sweepexpired)                             | Deletes records nothing reads any more (expired sessions, protocol artifacts, old deliveries) so storage stops growing with traffic.  |
| [`useProtocol`](#useprotocol)                               | Mounts a protocol service (OAuth sign-in, the OAuth authorization server, SAML, or SCIM) so the HTTP handlers route its paths to it.  |

### archiveAudit [#archiveaudit]

Copies every tenant's new audit events, verified and in chain order, to the configured `auditArchive` sink.

* **When:** on a schedule, every few minutes and at least hourly.
* **CLI:** [`audit-archive`](/docs/reference/cli#audit-archive).
* **Permission:** none. A deployment operation.
* **Audited as:** not audited. Progress is kept in one archive cursor per tenant.
* **Errors:** `NO_AUDIT_ARCHIVE` (501) when no `auditArchive` is configured; `INVALID_INPUT` for `limit` outside 1 to
  10,000,000.
* **Safe to repeat:** yes. One run at a time holds a tenant through a renewable lease; a tenant another run holds is
  skipped and listed under `busy`.

Each run reads the events after each tenant's cursor in batches, verifies every batch against the hash the previous
one ended with, hands it to your sink's `write`, and moves the cursor only after `write` resolves. A crash can
therefore hand the sink the same range again, so a sink must key batches by tenant and sequence range and throw
rather than replace a stored batch with different content; `createJsonlAuditArchive` does both (see
[continuous audit archiving](/docs/operations/jobs#continuous-audit-archiving)).

Problems do not throw. They are reported per tenant under `failed` with the code `AUDIT_CHAIN_BROKEN` (the chain does
not verify, and nothing past the break is archived), `ARCHIVE_WRITE_FAILED`, or `ARCHIVE_CONFLICT`, and sequences
deleted before they were archived appear under `gaps`. A run archives at most `limit` events (default 100,000);
`truncated: true` means run it again.

```ts
const result = await iam.archiveAudit();
if (result.failed.length) alerts.error('Audit archive is failing', result.failed);
```

```ts title="Signature"
iam.archiveAudit(
  input?: { tenantId?: string; limit?: number } | undefined,
): Promise<AuditArchiveResult>
```

### assertionKey [#assertionkey]

Returns the key downstream services use to verify the assertions this deployment issues.

* **When:** when you configure a downstream service. Never send it to a browser.
* **Permission:** none. A deployment secret, never exposed over HTTP.

[Assertions](/docs/reference/api/assertions#issue) are short-lived HS256 JSON Web Tokens that describe the caller
to another service. The key is a SHA-256 derivation of the deployment `secret`, as 64 hex characters: a service that
holds it verifies assertions with `verifyAssertion`, without a database round trip, and cannot recover the secret.
HS256 is symmetric, so whoever holds the key can also mint assertions that every other holder accepts: give it only
to services you trust and store it like a secret. It changes when `secret` changes; during a rotation, hand out
`assertionKeys()` instead.

```ts
// In the IAM deployment, once, to configure the reports service:
const reportsKey = iam.assertionKey();

// In the reports service, which holds only that key:
import { verifyAssertion } from 'better-iam';
const claims = verifyAssertion(token, { key: process.env.IAM_ASSERTION_KEY!, audience: 'reports' });
```

```ts title="Signature"
iam.assertionKey(): string
```

### assertionKeys [#assertionkeys]

Returns the assertion key of the current secret followed by those of `previousSecrets`, for verifiers during a secret rotation.

* **When:** while the deployment secret rotates.
* **Permission:** none. Deployment secrets, never exposed over HTTP.

New assertions are always signed with the current `secret`, but a verifier configured before a switch must accept
tokens signed on either side of it while configurations roll out. `verifyAssertion`, the NestJS assertion module, and
the Next.js edge verifier all accept this list. Hand it to verifiers when you introduce the new secret, and replace it
with `assertionKey()` alone when you retire `previousSecrets` (see
[rotating the deployment secret](/docs/operations/deployment/secrets)). If the old secret leaked, never give its key
to new verifiers.

```ts
// In the IAM deployment: every key, current first, for the downstream secret store.
const keys = iam.assertionKeys();

// In the reports service, while the rotation is in progress:
import { verifyAssertion } from 'better-iam';
const claims = verifyAssertion(token, {
  key: process.env.IAM_ASSERTION_KEYS!.split(','),
  audience: 'reports',
});
```

```ts title="Signature"
iam.assertionKeys(): string[]
```

### authenticate [#authenticate]

Resolves a credential to the identity and session behind it, re-checking everything that could have revoked it.

* **When:** in the request path, when you need to know who is calling (to render a profile, choose a tenant, or pass
  the principal on) without checking a specific permission.
* **Permission:** none. It proves identity, not access.
* **Audited as:** nothing, except `auth:session:mismatch` when a session bound to its sign-in network is presented
  from another address.
* **Errors:** `UNAUTHENTICATED` for a missing, malformed, expired, idle, or revoked credential, or a disabled or
  expired identity; `MFA_REQUIRED` when the session still owes MFA; `EMAIL_UNVERIFIED` when email verification is
  required and missing; `TENANT_UNAVAILABLE` or `TENANT_INACTIVE` when the organization is not active;
  `IP_NOT_ALLOWED` or `IP_BLOCKED` when network rules refuse the session.

Pass `{ token }`, or the incoming `{ headers }` (the session cookie or an `Authorization: Bearer` value). Every
credential kind works: user sessions, API keys, assumed-role sessions, session tokens, and IAM-signed session JWTs.
Each is backed by a stored session row, so revoking the row ends the credential at its next use. When you pass
headers, the client address is derived from them as the HTTP handler would (`http.clientInfo`), so network
allowlists and blocks apply. The result is `{ identity, session }`; `session.kind`, `session.mfa`, and
`session.tenantId` describe the caller. It checks no permission, so follow it with `authorize` or `require` before
acting on data. See [sessions](/docs/authentication/sessions).

```ts
const { identity, session } = await iam.authenticate({ headers: request.headers });
return Response.json({ name: identity.name, tenantId: session.tenantId, mfa: session.mfa });
```

```ts title="Signature"
iam.authenticate(
  input: CredentialInput,
): Promise<AuthenticatedPrincipal>
```

### authorize [#authorize]

Decides whether the caller may perform one action on one resource and returns the decision instead of throwing.

* **When:** in the request path, before you read or change a product resource. Also served as
  `POST {basePath}/authorize` and `client.authorize(input)`.
* **Permission:** none to call. The credential in the request (`token` or `headers`) is the principal being checked.
* **Audited as:** the requested action, only for denials (outcome `deny`) and root overrides. Allowed checks are not
  recorded.
* **Errors:** `UNAUTHENTICATED` when the credential is missing, expired, idle, or revoked; `MFA_REQUIRED` when the
  session still owes MFA; `TENANT_UNAVAILABLE` or `TENANT_INACTIVE` when the caller's organization or one of its
  ancestors is not active; `NOT_FOUND` for an unknown tenant or a managed resource that is not registered;
  `RESOURCE_RESOLVER_REQUIRED` for an application resource type without the `resolveResource` option;
  `RESOURCE_MISMATCH` when `resolveResource` returns a different resource.

The result is `{ allowed, reason, matched }`. An allow carries the reason it was granted. Every denial reports
`ACCESS_DENIED` with an empty `matched`, so a caller learns nothing about which rule refused it. The check reads
current state in one transaction (bindings, groups, policies, [conditions](/docs/authorization/conditions),
boundaries, relationships, and the tenant tree), and there is no permission cache to invalidate.

* An action the catalog does not know is denied, not an error.
* A root administrator signed in with MFA is allowed everything (`ROOT_OVERRIDE`), and each override is audited.
* Any other credential only works in its own organization: asking about another `tenantId` is denied.
* An [impersonation](/docs/authentication/impersonation) session is allowed only what both the member and the
  administrator behind it may do.
* With the `accessUsage` option on, each allowed check counts as usage for
  [role mining](/docs/features/governance/usage-and-mining).
* With the `clearances` option on, a resource carrying a [classification label](/docs/reference/api/clearances) is
  refused first, before any role or policy is read, unless every party of the session (the person, an agent key's
  sponsor, each agent of a delegated session, the administrator behind "view as") holds a clearance that dominates it.
  This applies to root administrators too, unless `clearances.appliesToRoot` is false. The refusal is reported as
  `ACCESS_DENIED` like any other and audited with the metadata `{ mandatory: 'clearance' }`; `iam:*` actions are never
  subject to labels.

```ts
const decision = await iam.authorize({
  headers: request.headers,
  tenantId,
  action: 'documents:write',
  resource: { type: 'document', id: documentId },
});
if (!decision.allowed) return new Response('Forbidden', { status: 403 });
```

```ts title="Signature"
iam.authorize(
  request: AuthorizationRequest,
): Promise<Decision>
```

### authorizeMany [#authorizemany]

Evaluates up to 50 checks for one caller in one tenant in a single transaction, for rendering UI state.

* **When:** in the request path, typically while a page decides which buttons and menu items to show. Also served as
  `POST {basePath}/authorizeMany` and `client.authorizeMany(input)`.
* **Permission:** none to call. Every check is evaluated for the request's credential.
* **Audited as:** each check's action, for denials and root overrides only.
* **Errors:** `INVALID_INPUT` for no checks, more than 50, or a check without `action`, `resource.type`, or
  `resource.id`; otherwise the errors of [`authorize`](#authorize).

Results come back in request order as `{ action, resource, allowed, reason }`, with the same rules as `authorize`.
Treat them as advisory: they say what the caller could do when the page rendered, so enforce each action again with
`authorize` or `require` when it is performed. One principal validation and one transaction serve every check, which
is cheaper than separate calls. Denied checks are recorded like any denied `authorize`, so a page that probes many
forbidden actions writes one audit event per denial.

```ts
const { results } = await iam.authorizeMany({
  headers: request.headers,
  tenantId,
  checks: [
    { action: 'documents:write', resource: { type: 'document', id: documentId } },
    { action: 'documents:delete', resource: { type: 'document', id: documentId } },
  ],
});
const [canEdit, canDelete] = results.map((result) => result.allowed);
```

```ts title="Signature"
iam.authorizeMany(
  request: BatchAuthorizationRequest,
): Promise<{ results: BatchResult[] }>
```

### bootstrap [#bootstrap]

Creates the platform's root tenant and its first root administrator, once per installation.

* **When:** once, right after the first `initialize`.
* **CLI:** [`bootstrap`](/docs/reference/cli#bootstrap), which reads the `BETTER_IAM_ROOT_*` environment variables.
* **Permission:** none. A deployment operation, never exposed over HTTP.
* **Audited as:** `root:bootstrap`, by `deployment-operator`.
* **Errors:** `ALREADY_INITIALIZED` (409) when a root tenant exists; `WEAK_PASSWORD` for a password shorter than 12
  characters or refused by the password policy, `BREACHED_PASSWORD` when a configured breach check matches it;
  `SLUG_TAKEN` when `slug` is in use; `INVALID_INPUT` for a malformed email or an empty name.
* **Safe to repeat:** yes. Every call after the first fails with `ALREADY_INITIALIZED` and changes nothing.

In one transaction it creates the root tenant (named `rootName`, default "Platform", with an optional `slug` alias
for sign-in screens), an identity that owns it and is a root administrator with a verified email, and an
unrestricted grant authority for that identity. The result's `mfaEnrollmentRequired: true` is a reminder that root
administrators always need MFA: the first sign-in enrolls a factor before the account can do anything. Read the
password from a secret store; the CLI takes it from the environment so it never appears in a command line.

```ts
const { tenant, identity } = await iam.bootstrap({
  email: 'platform-admin@example.com',
  name: 'Platform administrator',
  password: process.env.BETTER_IAM_ROOT_PASSWORD!,
  slug: 'platform',
});
```

```ts title="Signature"
iam.bootstrap(
  input: {
    email: string;
    name: string;
    password: string;
    rootName?: string;
    slug?: string;
  },
): Promise<{ tenant: Tenant; identity: PublicIdentity; mfaEnrollmentRequired: true }>
```

### callPlugin [#callplugin]

Runs a plugin endpoint from server code inside the same authorized, audited transaction as a built-in method.

* **When:** in the request path. The same endpoint is served as `POST {basePath}/plugins/{pluginId}/{path}`, and the
  browser client reaches it with `client.$request('plugins/{pluginId}/{path}', input)`.
* **Permission:** the endpoint's declared `action`, on `iam/{tenantId}`.
* **Audited as:** the endpoint's `action`.
* **Errors:** `NOT_FOUND` when no plugin with that id has a `POST` endpoint at `path`; `INVALID_INPUT` when
  `input.tenantId` differs from `tenantId` or the endpoint's `validate` rejects the input; `ACCESS_DENIED` without the
  action; plus whatever the endpoint's handler throws.

[Plugins](/docs/concepts/extensions) add endpoints of their own. `callPlugin` finds the endpoint by `pluginId` and
`path`, runs its `validate` on `input` (with `tenantId` added), and then runs its handler in the operation envelope:
the principal is re-validated, the action is authorized, plugin `beforeOperation` and `afterOperation` hooks run, and
the handler's writes commit together with one audit event, or not at all. The handler's return value is returned
unchanged.

```ts
const project = await iam.callPlugin(
  { headers: request.headers },
  { pluginId: 'projects', path: 'create', tenantId, input: { name: 'Website relaunch' } },
);
```

```ts title="Signature"
iam.callPlugin(
  credential: CredentialInput,
  input: { pluginId: string; path: string; tenantId: string; input?: unknown },
): Promise<unknown>
```

### checkInvariants [#checkinvariants]

Evaluates the access invariants of every active organization and records an audit event when one breaks or recovers.

* **When:** on a schedule, hourly and after configuration changes.
* **CLI:** [`monitor-invariants`](/docs/reference/cli#monitor-invariants).
* **Permission:** none. A deployment operation.
* **Audited as:** `invariant:broken` (outcome `deny`) when an invariant starts failing or gains violators, and
  `invariant:restored` when it passes again, by `deployment-operator`.
* **Safe to repeat:** yes. The outcome is stored on each invariant (`lastCheck`) and events are recorded only when it
  changes; each organization is evaluated in its own transaction.

Enforced invariants already refuse changes that would break them. This job is how `monitor` invariants, and
violations that predate enforcement, reach you: a webhook subscribed to `invariant:*` alerts once per change instead
of every hour. The result is `{ checked, broken, restored }`. For a CI gate that fails a build, use
[`invariants.run`](/docs/reference/api/invariants#run) (CLI `check-invariants`). See
[change safety](/docs/features/governance/change-safety).

```ts title="Signature"
iam.checkInvariants(
  input?: { tenantId?: string } | undefined,
): Promise<InvariantCheckResult>
```

### closeOverdueCertifications [#closeoverduecertifications]

Closes every auto-closing certification campaign whose due date has passed and applies its decisions.

* **When:** on a schedule, hourly or daily with the other jobs.
* **CLI:** [`close-certifications`](/docs/reference/cli#close-certifications).
* **Permission:** none to call. Revocations run under the campaign creator's grant authority.
* **Audited as:** `certification:auto-close` for each campaign, with the outcome counts, plus `iam:bindings:delete`
  for each removed binding, by `deployment-operator`.
* **Errors:** `NOT_FOUND` for an unknown `tenantId`.
* **Safe to repeat:** yes. Each campaign is re-read and closed in its own transaction, so a campaign an administrator
  closed meanwhile is left alone and none is closed twice.

Only [campaigns](/docs/features/governance/certifications) created with `autoClose` and a `dueAt` are affected. Revoked
bindings are removed, and items nobody decided follow the campaign's `undecided` setting. When the creator no longer
exists the campaign still closes, and every revocation is reported as `revocation-failed`. The result is
`{ closed, skipped }`, where `skipped` counts open auto-closing campaigns that are not due yet.

```ts title="Signature"
iam.closeOverdueCertifications(
  input?: { tenantId?: string } | undefined,
): Promise<CertificationAutoCloseResult>
```

### closeOverdueTeamReviews [#closeoverdueteamreviews]

Completes every open team membership review whose due date has passed and removes the people it decided to remove.

* **When:** on a schedule, hourly or daily with the other jobs.
* **Permission:** none. A deployment operation.
* **Audited as:** `team:member:remove` (with `source: review` and the `reviewId`) for each person removed and
  `team:review:complete` with the counts, by `deployment-operator`.
* **Safe to repeat:** yes. Each review is re-read and completed in its own transaction, so a review someone completed
  or cancelled meanwhile is left alone and none is completed twice.

It looks at the open reviews past their `dueAt` (of every organization, or only `tenantId`) and skips those of
organizations that are not active or of teams that no longer exist. Completing a review works as
[`teams.completeReview`](/docs/reference/api/teams#completereview) does: people decided `remove` leave the team,
people nobody decided on follow the review's `onUndecided`, and people who already left or whom team sync now manages
are counted as `gone`. The birthright packages of the people removed are re-evaluated afterwards. A review that fails
does not stop the others. The result is `{ completed, removed, failed }`, where `failed` lists the `reviewId` and
`message` of each review that could not be completed.

```ts
const { completed, failed } = await iam.closeOverdueTeamReviews();
if (failed.length) await notifyAdmins(failed);
```

```ts title="Signature"
iam.closeOverdueTeamReviews(
  input?: { tenantId?: string } | undefined,
): Promise<{
  completed: number;
  removed: number;
  failed: Array<{ reviewId: string; message: string }>;
}>
```

### detectThreats [#detectthreats]

Reads each active organization's new audit events, verifies their hash chain, and raises threat detections, incidents, and responses.

* **When:** on a schedule, every minute. The interval is how late a detection can be.
* **CLI:** [`detect-threats`](/docs/reference/cli#detect-threats).
* **Permission:** none. A deployment operation.
* **Audited as:** `threat:detection` and `threat:incident-open` for what it finds, `threat:risk-change` when an
  identity's risk level moves, and the playbook responses it takes (`threat:contain`, `threat:revoke-sessions`,
  `threat:block-network`, `threat:notify`, `threat:response-braked`, and so on), all by the actor `threat-detection`.
* **Errors:** `NOT_FOUND` for an unknown `tenantId`; `INVALID_INPUT` for a `maxEvents` outside 1 to 20,000.
* **Safe to repeat:** yes. Each tenant has a cursor, the sequence and hash of the last event read, and every
  detection a unique key, so re-reading events or overlapping runs never records anything twice.

For every active organization (or only `tenantId`) it reads at most `maxEvents` unread events (2000 by default) from
the tenant's cursor; a tenant's first run starts one day back. It checks that each event follows the one before and
that its hash recomputes, and reports a break as an `audit-tampering` detection without stopping. Then it evaluates
the tenant's detection rules over the new events and the history their windows reach back to, records detections
grouped into incidents, raises identities' risk (which policies read as `principal.riskLevel` and
`principal.riskScore`), and runs the tenant's playbooks for new detections, with at most `maxAutomaticContainments`
automatic containments per tenant and run. The module's own events are verified but never judged. A tenant whose
evaluation fails records nothing and keeps its cursor, and the others carry on. The result is
`{ tenants, eventsScanned, detections, incidentsOpened, responses, braked, chainBreaks, pending }`, where `pending`
counts tenants with more unread events than one run reads. Rules, incidents, and playbooks are managed through the
[threats group](/docs/reference/api/threats), whose `detect` runs one tenant on demand.

```ts
setInterval(async () => {
  const run = await iam.detectThreats();
  if (run.chainBreaks) await alertSecurityTeam('Audit chain failed verification', run);
}, 60_000).unref();
```

```ts title="Signature"
iam.detectThreats(
  input?: { tenantId?: string; maxEvents?: number } | undefined,
): Promise<ThreatDetectionRun>
```

### dispatchAuditHooks [#dispatchaudithooks]

Runs plugin `afterAudit` hooks, `events.onEvent`, and in-process subscribers for audit events that have committed.

* **When:** on a schedule, every minute, in the process that registers your subscribers. It is the same function as
  `iam.events.dispatch()`.
* **Permission:** none.
* **Errors:** whatever a handler throws. The run stops there and the event stays queued.
* **Safe to repeat:** yes, but delivery is at least once. A throwing handler leaves its event queued, and two
  dispatchers running at once can both deliver the same event, so make handlers idempotent by `event.id`.

Handlers never run inside the request. The process that records an audit event queues it in the same transaction,
but only when that process has an `iam.events.subscribe` handler, an `events.onEvent` option, or a plugin with
`afterAudit`. This function delivers the queue oldest first and marks each row delivered, and those rows are shared
by every process. The CLI's `outbox` command dispatches too, but a CLI process only has the hooks in its
configuration file, so events it dispatches never reach subscribers your application registered: dispatch in the
application process instead. Webhooks are separate and travel through the outbox (`iam.auth.dispatchOutbox()`). The
result is `{ dispatched }`. See [lifecycle events](/docs/concepts/events/lifecycle-events).

```ts
iam.events.subscribe('iam:identities:*', async (event) => {
  await searchIndex.refreshIdentity(event.resourceId);
});
setInterval(() => void iam.dispatchAuditHooks().catch(reportError), 60_000);
```

```ts title="Signature"
iam.dispatchAuditHooks(): Promise<{ dispatched: number }>
```

### flushAccessUsage [#flushaccessusage]

Writes the access usage buffered in memory to storage now.

* **When:** at shutdown (for example on `SIGTERM`). Buffered usage is also written on its own every `flushIntervalMs`
  (one minute by default) and when `maxBuffered` pairs are waiting.
* **Permission:** none.
* **Audited as:** not audited.
* **Safe to repeat:** yes. Concurrent calls queue behind one write, and a failed write keeps its batch for the next
  attempt before rethrowing.

With the `accessUsage` option on, every allowed `authorize`, `authorizeMany`, `listAccessible`, and provisioning
operation is counted in memory per person and action (root overrides and impersonation excepted) and written in
batches, so no request waits on storage. Without a final flush, the last interval of usage is lost when the process
exits. The result is `{ written }`, the number of person and action pairs written; with tracking off it writes
nothing. See [usage and role mining](/docs/features/governance/usage-and-mining).

```ts
process.on('SIGTERM', async () => {
  await iam.flushAccessUsage();
  process.exit(0);
});
```

```ts title="Signature"
iam.flushAccessUsage(): Promise<{ written: number }>
```

### initialize [#initialize]

Creates or upgrades the database schema and runs one-time data upgrades so the instance can serve requests.

* **When:** once per deploy, before the new version serves traffic.
* **CLI:** [`migrate`](/docs/reference/cli#migrate).
* **Permission:** none. A deployment operation, never exposed over HTTP.
* **Errors:** `SCHEMA_VERSION` when the database carries a schema version this release does not support.
* **Safe to repeat:** yes. Every step is idempotent, and instances that migrate at the same time wait for each
  other's lock (up to ten minutes) instead of failing.

In order, it applies the storage adapter's schema migrations, runs each plugin's `migrate` in its own transaction,
starts the retention window of tenants deleted before `deletedAt` existed, and chains audit events recorded before
the hash chain existed (once, in timestamp order per tenant). Some upgrades build indexes inside the migration
transaction, which blocks IAM writes (not reads) on large tables while it runs, and the first run after upgrading to
the chained audit log backfills it in one transaction. Plan those deploys for a maintenance window; see
[storage](/docs/databases/operations).

```ts
// In a release step, before the new version starts serving.
await iam.initialize();
```

```ts title="Signature"
iam.initialize(): Promise<void>
```

### listAccessible [#listaccessible]

Lists the registered resources of one managed type that the caller may perform an action on, one page at a time.

* **When:** in the request path, for list pages such as "my workspaces". Also served as
  `POST {basePath}/listAccessible` and `client.listAccessible(input)`.
* **Permission:** none to call. Results are filtered for the request's credential.
* **Audited as:** not audited.
* **Errors:** `INVALID_ACTION` for an action the catalog does not know; `INVALID_RESOURCE_TYPE` for an unknown type or
  one your application resolves itself; `INVALID_INPUT` for `limit` outside 1 to 1000 or `offset` outside 0 to
  1,000,000; `NOT_FOUND` for an unknown tenant; plus the credential errors of [`authorize`](#authorize).

It is the reverse of `authorize`: instead of asking about one resource, it evaluates every
[registered resource](/docs/concepts/resources-and-catalog) of `type` against the caller's grants,
boundaries, and conditions, which are loaded once. `total` counts every accessible resource and `resources` holds
the requested page (`limit` defaults to 100, `offset` to 0) in a stable order. A root administrator sees them all,
an impersonation session sees only what the administrator behind it could reach, and a caller from another
organization gets an empty list. It only works for managed types, because application-resolved resources are not
stored in IAM. Like `authorizeMany`, the list is advisory: check the action again when the person opens an item.
With the `clearances` option, the type's classification labels are read once and every resource is checked against
them as `authorize` would, so resources labeled above the caller's clearance are left out, root administrators'
included unless `clearances.appliesToRoot` is false.

```ts
const { resources, total } = await iam.listAccessible({
  headers: request.headers,
  tenantId,
  action: 'workspaces:read',
  type: 'workspace',
  limit: 25,
  offset: 0,
});
```

```ts title="Signature"
iam.listAccessible(
  request: AccessibleResourcesRequest,
): Promise<{ resources: ResourceRecord[]; total: number }>
```

### planResources [#planresources]

Returns which resources of one type the caller may perform an action on, as a filter for your own database query.

* **When:** in the request path, for list pages over application data (the rows live in your database, not in IAM).
  Also served as [`filters.plan`](/docs/reference/api/filters#plan) over HTTP.
* **Permission:** none to call. The plan is the request credential's.
* **Audited as:** not audited.
* **Errors:** `INVALID_INPUT` for an `iam:*` action or an internal type; `UNSUPPORTED_FILTER` when the plan cannot be
  exact, including the security clearance cases below; plus the credential errors of [`authorize`](#authorize).

Where `listAccessible` evaluates the resources IAM stores, a plan works for any type: the server partially evaluates
the caller's grants, denies, boundaries, relationships and session limits and returns `always`, `never`, or
`conditional` with a filter over `id` and the resource's attributes, with the same result `authorize` would give for
each row. Compile it with `filterToSql`, `filterToPrisma`, `filterToMongo` or `filterMatches` from `@better-iam/core`.

With the `clearances` option, every plan (a root administrator's too) is AND-ed with a filter over `id` that leaves
out the resources labeled above the caller's clearance, which compiles to every target. A plan answers
`UNSUPPORTED_FILTER` for an application type the scheme requires labels on, for any application type while a label in
the tenant passes down to children, and when a statement conditions on a label's resource keys; labels your
`resolveResource` asserts are not visible to plans, so check such rows with `authorize`
([security clearances](/docs/reference/api/clearances)).

```ts
import { filterToSql } from 'better-iam/core';

const plan = await iam.planResources({ headers: request.headers, tenantId, action: 'documents:read', type: 'document' });
if (plan.kind === 'never') return [];
const where = filterToSql(plan.filter, { dialect: 'postgres', column: (field) => columns[field] });
```

```ts title="Signature"
iam.planResources(
  request: PlanResourcesRequest,
): Promise<ResourcePlanResult>
```

### pruneAudit [#pruneaudit]

Deletes one tenant's audit events older than a retention period while keeping the rest of the chain verifiable.

* **When:** on a schedule that matches your retention policy, after the events were archived or exported.
* **CLI:** [`audit-prune`](/docs/reference/cli#audit-prune).
* **Permission:** none. A deployment operation, with no tenant check.
* **Audited as:** `audit:prune` on the tenant, by `deployment-operator`, with the number deleted, the cutoff, and the
  sequence and hash the chain now starts after.
* **Errors:** `INVALID_INPUT` for a missing `tenantId` or a `retentionMs` outside 0 to 100 years.
* **Safe to repeat:** yes. It runs in one transaction; a rerun with the same retention deletes only events that have
  aged since, and records nothing when nothing was deleted.

It deletes the longest run of oldest events, in chain order, whose timestamps are before now minus `retentionMs`,
stopping at the first newer event, then appends the `audit:prune` checkpoint so
[chain verification](/docs/concepts/events/audit-chain) starts after the pruned prefix. Once a tenant has an archive
cursor, or wherever `auditArchive` is configured, it deletes only events the archive already holds, in every process,
and returns `heldForArchive: true` when it stopped early for that reason. Pruned events are gone from the database,
so archive or export them first.

```ts
const { deleted, heldForArchive } = await iam.pruneAudit({
  tenantId,
  retentionMs: 365 * 24 * 60 * 60 * 1000,
});
```

```ts title="Signature"
iam.pruneAudit(
  input: { tenantId: string; retentionMs: number },
): Promise<{
  deleted: number;
  prunedThroughSequence?: number;
  prunedThroughHash?: string;
  heldForArchive?: true;
}>
```

### purgeDeleted [#purgedeleted]

Runs the retention worker: ends access whose time is up and removes deleted tenants past their retention window.

* **When:** on a schedule, hourly or at least daily.
* **CLI:** [`purge`](/docs/reference/cli#purge).
* **Permission:** none. A deployment operation.
* **Audited as:** `identity:expire` for each identity it disables and `tenants:purge` for each purged tenant tree, by
  `deployment-operator`. Audit records of purged tenants are kept.
* **Errors:** `INVALID_INPUT` when `retentionMs` is outside 0 to ten years.
* **Safe to repeat:** yes. Authentication bookkeeping is deleted in short batches that re-read each row, and the rest
  runs in one transaction, so a second or overlapping run finds nothing left to do.

In one run it:

* deletes expired temporary role bindings with their activations, and ended just-in-time activations;
* removes lapsed temporary group memberships (with the activations of the group's eligible bindings they carried) and
  access-package assignments past their end;
* marks pending access requests and package requests past their lifetime as `expired`;
* disables identities past their scheduled deactivation (`expiresAt`), revoking their sessions and activations;
* deletes rate-limit counters past their window, expired challenges, and lapsed network blocks;
* deletes every record of tenants tombstoned more than `retentionMs` ago (default 30 days), their descendants,
  plugin-owned records through plugin `purge` callbacks, and references to them from surviving tenants.

Expired access is refused at its next use even before the worker runs, so the schedule only decides how quickly
statuses, lists, and reports catch up. The result counts `purgedTenants`, `deletedRecords`, `expiredBindings`,
`expiredRequests`, `expiredIdentities`, `expiredActivations`, `expiredMemberships`, and `expiredAssignments`. See
[access lifecycle](/docs/features/privileged-access/lifecycle).

```ts
const result = await iam.purgeDeleted({ retentionMs: 30 * 24 * 60 * 60 * 1000 });
```

```ts title="Signature"
iam.purgeDeleted(
  input?: { retentionMs?: number } | undefined,
): Promise<PurgeResult>
```

### reconcilePackages [#reconcilepackages]

Applies access-package rules: people who match a rule receive the package, and automatic holders who stopped matching lose it.

* **When:** on a schedule, every 15 minutes, after `purgeDeleted`.
* **CLI:** [`reconcile`](/docs/reference/cli#reconcile).
* **Permission:** none to call. Each change runs under the rule owner's grant authority, so a rule never grants more
  than its owner could.
* **Audited as:** `package:auto-assign`, `package:auto-ending`, and `package:auto-revoke` for each change, plus
  `package:auto-suspended`, `package:auto-braked`, `package:auto-failed`, `package:auto-resumed`, and
  `package:auto-confirm` for rule problems and confirmations.
* **Errors:** `INVALID_INPUT` for a `packageId` without `tenantId`, `confirm` without `packageId`, or a `limit`
  outside 1 to 10,000; `NOT_FOUND` for an unknown `tenantId`.
* **Safe to repeat:** yes. It plans from plain reads and applies each change in its own transaction; a change whose
  inputs moved in between is counted as `stale` and retried on the next run.

This is how [automatic assignment](/docs/features/privileged-access/automatic-assignment) (birthright access) keeps up
with SCIM provisioning, invitations, attribute changes, and group changes. Removals run first and honor the rule's
grace period (`ending`). A run makes at most `limit` changes per organization (default 1000); `truncated: true` means
run it again. Scheduled runs hold back unusually large changes (listed under `braked`) until a person confirms them in
the console or with `confirm: true` for one `packageId`. A rule that cannot be evaluated is `suspended` instead of
revoking everyone, and an organization that fails is listed under `skipped.failedTenants` without stopping the
others.

```ts
const result = await iam.reconcilePackages();
if (result.failed.length || result.suspended.length || result.braked.length) await notifyAdmins(result);
```

```ts title="Signature"
iam.reconcilePackages(
  input?: | { tenantId?: string; packageId?: string; limit?: number; confirm?: boolean }
    | undefined,
): Promise<PackageReconcileResult>
```

### recoverRoot [#recoverroot]

Creates an additional root administrator in the root tenant when nobody can sign in as root any more.

* **When:** during an incident, from a trusted shell with the production configuration.
* **CLI:** [`recover-root`](/docs/reference/cli#recover-root), which reads the `BETTER_IAM_ROOT_*` environment
  variables.
* **Permission:** none. A deployment operation, never exposed over HTTP: access to the database and configuration is
  the authority.
* **Audited as:** `root:recover`, by `deployment-operator`.
* **Errors:** `NOT_INITIALIZED` before `bootstrap` has run; `IDENTITY_EXISTS` when the email already belongs to an
  identity in the root tenant; `WEAK_PASSWORD` or `BREACHED_PASSWORD` for a password the policy refuses.
* **Safe to repeat:** each call adds another administrator and another `root:recover` event.

It does not reset or unlock existing administrators. It adds a new identity with a verified email and the root
administrator flag, which enrolls MFA at its first sign-in, so use an email address that is not in the root tenant
yet. Once you are back in, review the [root administrators](/docs/reference/api/root#listadministrators) and repair
or remove the lost account. Treat it as a break-glass procedure and alert on `root:recover` events.

```ts title="Signature"
iam.recoverRoot(
  input: { email: string; name: string; password: string },
): Promise<{ identity: PublicIdentity; tenantId: string; mfaEnrollmentRequired: true }>
```

### require [#require]

Checks one action like `authorize` and throws `ACCESS_DENIED` (403) when it is not allowed.

* **When:** in the request path, as a guard at the top of a route handler or server action.
* **Permission:** none to call. The request's credential is checked.
* **Audited as:** the requested action, for denials and root overrides.
* **Errors:** `ACCESS_DENIED` when the decision is a denial; otherwise the errors of [`authorize`](#authorize).

Use `require` when a denial should end the request, and `authorize` when you want to branch on the decision.
Framework guards and middleware call it for you. Map the thrown `IamError` to a response by its `code`, never its
message.

```ts
await iam.require({
  headers: request.headers,
  tenantId,
  action: 'invoices:approve',
  resource: { type: 'invoice', id: invoiceId },
});
```

```ts title="Signature"
iam.require(
  request: AuthorizationRequest,
): Promise<void>
```

### rotateSecrets [#rotatesecrets]

Re-seals values encrypted with a previous deployment secret using the current one, so the old secret can be retired.

* **When:** during a secret rotation, after every process runs with the new `secret` and the old value in
  `previousSecrets`.
* **CLI:** [`rotate-secrets`](/docs/reference/cli#rotate-secrets).
* **Permission:** none. A deployment operation.
* **Audited as:** not audited.
* **Errors:** `INVALID_INPUT` for a `batchSize` outside 1 to 2000 or a `limit` that is not a positive integer.
* **Safe to repeat:** yes. It works a page at a time in short transactions that re-read each record, so you can stop
  it and run it again.

It rewrites TOTP authenticator secrets, webhook signing secrets, and the sealed payloads of undelivered outbox
messages. Sessions, API keys, and invitation links do not depend on the secret, so nobody is signed out.
`dryRun: true` only counts. The result has `resealed` and `unreadable` counts per collection, `current` (values
already sealed with the current secret), `complete`, and `done`. Repeat until `done` is true, wait a day for emailed
links and assertions issued under the old secret to expire, then remove `previousSecrets` everywhere.
`unreadable` values open with no configured secret, which means the secret was replaced without listing the old one:
put it back into `previousSecrets` and run again. See
[rotating the deployment secret](/docs/operations/deployment/secrets).

```ts
const preview = await iam.rotateSecrets({ dryRun: true });
if (!preview.done) await iam.rotateSecrets();
```

```ts title="Signature"
iam.rotateSecrets(
  options?: SecretRotationOptions | undefined,
): Promise<SecretRotationResult>
```

### selfCheck [#selfcheck]

Reports configuration and storage problems an operator should act on, each with a severity and a fix.

* **When:** after each deploy as a gate, and on a schedule as a health check. It only reads.
* **CLI:** [`doctor`](/docs/reference/cli#doctor).
* **Permission:** none. A deployment operation.
* **Errors:** `INVALID_INPUT` for `cap` outside 1 to 100,000 or a retention option out of range.

It returns `{ ok, findings, storage }`. `ok` is false when any finding is an `error`. `storage` is the adapter's own
description: schema version, applied migrations, record counts, and durability settings. Each finding has a stable
`check` id, a `severity`, a `message`, a `fix`, and sometimes a `count`:

* **Errors:** `schema-behind`, `not-bootstrapped`, `sqlite-durability` (a rollback journal with `synchronous` at
  NORMAL or OFF), and `unreadable-secrets`.
* **Warnings:** `in-memory-database`, `postgres-async-commit`, `weak-secret`, `weak-metrics-token`,
  `no-email-transport`, `secret-rotation-pending`, `secret-rotation-unverified`, and jobs that are not running:
  `sweep-backlog` (records due for more than two days), `purge-not-running` (expired bindings, memberships, or
  challenges over a day old), `outbox-stalled` (messages waiting over 15 minutes), `outbox-abandoned` (messages
  abandoned in the last day), `audit-hooks-stalled` (hooks waiting over 15 minutes), and `audit-archive-behind`
  (unarchived events older than a day, with `auditArchive`).
* **Info:** `storage-undescribed` and `previous-secrets-configured`.

Pass the `deliveryRetentionMs` and `graceMs` your sweep runs with so its backlog is judged the same way. `cap`
(default 1000) bounds how many records each check counts and how many sealed values per collection it samples.

```ts
const { ok, findings } = await iam.selfCheck({ deliveryRetentionMs: 30 * 24 * 60 * 60 * 1000 });
if (!ok) throw new Error(findings.map((finding) => `${finding.check}: ${finding.fix}`).join('\n'));
```

```ts title="Signature"
iam.selfCheck(
  options?: SelfCheckOptions | undefined,
): Promise<SelfCheckResult>
```

### sendAccessDigest [#sendaccessdigest]

Emails each organization's owners its access report when the report has something in it, at most once per interval.

* **When:** on a schedule, daily, followed by an outbox run that delivers the messages.
* **CLI:** [`digest`](/docs/reference/cli#digest).
* **Permission:** none. A deployment operation that includes every report section.
* **Audited as:** `tenant:access-digest` on each organization emailed, by `deployment-operator`, with the recipient
  and finding counts.
* **Errors:** `DELIVERY_REQUIRED` when `authentication.sendEmail` is not configured; `NOT_FOUND` for an unknown
  `tenantId`; `INVALID_INPUT` for a window out of range.
* **Safe to repeat:** yes. An organization digested within `minimumIntervalMs` (default 20 hours) is skipped, so
  reruns and overlapping schedules never email twice.

For every active organization, or one `tenantId`, it builds the
[access report](/docs/features/privileged-access/access-report): identities and temporary bindings ending within
`withinMs` (default 30 days), memberships ending soon, live just-in-time activations, pending activation requests,
and API keys unused for `unusedForMs` (default 30 days) or ending soon. Each active owner with an email address of an
organization with findings is sent one `access-digest` message with the counts and the full report as JSON. The
result lists `sent` per organization and `skipped` counts (`inactive`, `recent`, `quiet`, `noOwners`).

```ts
await iam.sendAccessDigest({ withinMs: 14 * 24 * 60 * 60 * 1000 });
await iam.auth.dispatchOutbox();
```

```ts title="Signature"
iam.sendAccessDigest(
  input?: | {
        tenantId?: string;
        withinMs?: number;
        unusedForMs?: number;
        minimumIntervalMs?: number;
      }
    | undefined,
): Promise<AccessDigestResult>
```

### sendExpiryReminders [#sendexpiryreminders]

Emails each person whose access ends soon one reminder listing it, once per item and end date.

* **When:** on a schedule, daily beside `sendAccessDigest`, followed by an outbox run.
* **CLI:** [`remind`](/docs/reference/cli#remind).
* **Permission:** none. A deployment operation.
* **Audited as:** `identity:expiry-reminder` on each person emailed, by `deployment-operator`, with the reminded items.
* **Errors:** `DELIVERY_REQUIRED` when `authentication.sendEmail` is not configured; `NOT_FOUND` for an unknown
  `tenantId`; `INVALID_INPUT` for a `withinMs` outside one minute to 365 days.
* **Safe to repeat:** yes. A reminder mark per item and end date prevents repeats.

It looks at every active person in every active organization, or one `tenantId`, and collects what ends within
`withinMs` (default seven days): their own account (`expiresAt`), direct role bindings, temporary group memberships,
and access-package assignments. Bindings and memberships that belong to a package are reminded as the package, group
bindings are left to the owners' digest, and eligible (just-in-time) bindings are skipped. Each person with something
new gets one `expiry-reminder` email with `items` as JSON (kind, name, and end). Extending access brings a fresh
reminder when the new end enters the window. People without an email address are skipped.

```ts
const { sent } = await iam.sendExpiryReminders({ withinMs: 3 * 24 * 60 * 60 * 1000 });
```

```ts title="Signature"
iam.sendExpiryReminders(
  input?: { tenantId?: string; withinMs?: number } | undefined,
): Promise<ExpiryReminderResult>
```

### sweepExpired [#sweepexpired]

Deletes records nothing reads any more (expired sessions, protocol artifacts, old deliveries) so storage stops growing with traffic.

* **When:** on a schedule, hourly or daily, beside `purgeDeleted`.
* **CLI:** [`sweep`](/docs/reference/cli#sweep).
* **Permission:** none. A deployment operation.
* **Audited as:** not audited.
* **Errors:** `INVALID_INPUT` for an option outside its range.
* **Safe to repeat:** yes. Each batch is read and deleted in its own short transaction, and records are judged on
  their current state, so it can run during traffic and beside another run.

It deletes user sessions, role sessions, and session tokens past their absolute expiry; trusted devices and
relationship tuples past expiry; redeemed web-identity tokens once they could no longer be presented; OAuth artifacts
and login states (OAuth grants 31 days after expiry, so back-channel logout still reaches the client); SAML request,
relay-state, and assertion-replay records; delivered or abandoned outbox messages and abandoned Shared Signals
deliveries older than `deliveryRetentionMs`; and audit hook rows already dispatched. API keys, pending deliveries,
invitations, access requests, usage records, and SCIM connections are never deleted by age.

Options: `limit` (default 10,000 records per run, at most 1,000,000), `batchSize` (500 per transaction, 1 to 5000),
`deliveryRetentionMs` (30 days, 0 to 3650 days), `graceMs` (a five-minute margin past expiry against clock skew, at
most one day), and `now` (for tests and backfills). The result is `{ deleted, total, truncated }` with counts per
collection; `truncated: true` means more records are due, so run it again. Pass the same `deliveryRetentionMs` to
[`selfCheck`](#selfcheck) so its backlog check agrees.

```ts
const { total, truncated } = await iam.sweepExpired({ deliveryRetentionMs: 14 * 24 * 60 * 60 * 1000 });
```

```ts title="Signature"
iam.sweepExpired(
  options?: SweepOptions | undefined,
): Promise<SweepResult>
```

### useProtocol [#useprotocol]

Mounts a protocol service (OAuth sign-in, the OAuth authorization server, SAML, or SCIM) so the HTTP handlers route its paths to it.

* **When:** once per service at process start, before the handler serves requests.
* **Permission:** none. The mounted service authenticates its own requests.

A protocol service is `{ basePath, handler, nodeHandler }` (either handler optional), as returned by
`createScimService`, `createSamlService`, `createOAuthLogin`, or `createOAuthProvider`, which you build from the
`iam.protocolHost` callbacks. `handler` asks each mounted service in mount order and uses the first response.
`nodeHandler` also offers requests under a service's `basePath` to that service's Node handler, which the OAuth
authorization server requires, so serve it through `iam.nodeHandler`. When a mounted service returns a session (a
SAML or OAuth sign-in), the handler sets the IAM session cookie, and every mount runs with the request's client
address, so network allowlists and blocks apply to the sessions it issues. There is no unmount, so call it once per
service. See [protocol mounts](/docs/operations/deployment/protocol-mounts).

```ts
import { createScimService } from 'better-iam/scim';

const scim = createScimService({ ...iam.protocolHost });
iam.useProtocol(scim);
```

```ts title="Signature"
iam.useProtocol(
  protocol: ProtocolService,
): void
```
