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

# Identity and auth

> An oracle has a persistent blockchain identity (entity DID + Matrix bot) and per-user auth via UCAN delegation. This guide covers both.

## What identity looks like in code

Two layers — the oracle's persistent identity, set once at boot via env vars; and per-user identity, validated on every request via UCAN headers.

```ts theme={"system"}
import { createOracleApp, type OracleConfig } from '@ixo/oracle-runtime';

const config: OracleConfig = {
  name: 'My Oracle',
  org: 'My Org',
  description: 'What this oracle does.',
};

const app = await createOracleApp({ config });
// Runtime fills in `entityDid` from process.env.ORACLE_ENTITY_DID
// and builds the internal OracleIdentity used by every plugin.
```

The `OracleConfig` type lives in [`packages/oracle-runtime/src/plugin-api/types.ts`](https://github.com/ixoworld/ixo-oracles-boilerplate/blob/main/packages/oracle-runtime/src/plugin-api/types.ts) — plugins read the resolved identity off `ctx.identity` (a [`OracleIdentity`](https://github.com/ixoworld/ixo-oracles-boilerplate/blob/main/packages/oracle-runtime/src/plugin-api/types.ts)).

| Layer               | What it is                              | Where it comes from                                                                                                                                                                           |
| ------------------- | --------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Oracle identity** | DID + Matrix bot user + on-chain entity | `OracleConfig` (inline) + `ORACLE_DID` / `ORACLE_ENTITY_DID` / `MATRIX_ORACLE_ADMIN_*` env vars                                                                                               |
| **User identity**   | Per-request DID + UCAN capabilities     | Headers on every incoming request — a user-signed UCAN invocation (`Authorization: Bearer …` + `X-Auth-Type: ucan`) for authentication, plus `x-ucan-delegation` for downstream authorization |

## One-time oracle setup

<Steps>
  <Step title="Run the CLI to create the oracle's on-chain entity and Matrix account">
    ```sh theme={"system"}
    qiforge-cli create-entity --no-interactive \
      --network devnet \
      --oracle-name "My Oracle" \
      --org-name "My Org" \
      --api-url http://localhost:3000 \
      --model "anthropic/claude-sonnet-4"
    ```

    This creates a blockchain entity (`did:ixo:entity:...`), registers linked resources, provisions a Matrix bot account, and writes both `oracle.config.json` and `.env`. Run once per deployment — subsequent deploys reuse the identity.

    <Note>
      `create-entity` registers `--api-url http://localhost:4000` by default, but the runtime's own default `PORT` is `3000` (the example above pins the URL to `:3000` to match). If you take the default URL instead, make your oracle reachable at it: either set `PORT=4000` in `.env`, or change the registered URL later with `qiforge-cli update-oracle-api-url`.
    </Note>

    See the [CLI reference](/build-an-oracle/reference/cli) for every flag.
  </Step>

  <Step title="Provision the encryption key for per-room secrets">
    ```sh theme={"system"}
    qiforge-cli setup-encryption-key
    ```

    Sets up the P-256 keyAgreement on the oracle's Matrix account room. Without this, the secrets read path returns nothing (acceptable degraded mode for routes that don't need it).
  </Step>

  <Step title="Verify .env has every identity var">
    After CLI setup, `.env` contains the core identity vars validated by [`baseEnvSchema`](https://github.com/ixoworld/ixo-oracles-boilerplate/blob/main/packages/oracle-runtime/src/config/base-env-schema.ts):

    ```text theme={"system"}
    ORACLE_DID=did:ixo:ixo1...
    ORACLE_ENTITY_DID=did:ixo:entity:...
    ORACLE_NAME=My Oracle
    NETWORK=devnet

    MATRIX_BASE_URL=https://matrix.ixo.world
    MATRIX_ORACLE_ADMIN_USER_ID=@oracle-bot:ixo.world
    MATRIX_ORACLE_ADMIN_PASSWORD=...
    MATRIX_ORACLE_ADMIN_ACCESS_TOKEN=...
    MATRIX_ACCOUNT_ROOM_ID=...
    MATRIX_VALUE_PIN=...
    MATRIX_RECOVERY_PHRASE=...

    SECP_MNEMONIC=...   # signs UCAN invocations to downstream services
    ```

    The runtime validates all of these at boot via [`baseEnvSchema`](https://github.com/ixoworld/ixo-oracles-boilerplate/blob/main/packages/oracle-runtime/src/config/base-env-schema.ts); missing vars fail with a clear message.

    Two auth-tuning vars have safe defaults — set them only to override:

    ```text theme={"system"}
    UCAN_AUTH_MAX_TTL_SECONDS=900           # max lifetime the oracle accepts for a user auth invocation (default 15 min)
    UCAN_REAUTH_PROMPT_THROTTLE_SECONDS=21600  # min gap between "please re-authorize" prompts (default 6 hours)
    ```

    `UCAN_AUTH_MAX_TTL_SECONDS` bounds the replay window server-side regardless of the TTL the client declares; `UCAN_REAUTH_PROMPT_THROTTLE_SECONDS` keeps a de-authorized user from being nagged on every message.
  </Step>
</Steps>

## What the runtime loads on boot

After Matrix init completes in the background, the runtime reads two further pieces of secret material from the oracle's Matrix account room:

1. **UCAN signing mnemonic** — used by `UcanService` to mint downstream-service invocations.
2. **P-256 encryption key** — used by `SecretsService` to decrypt per-room secrets.

If a key is missing, boot logs a warning naming the CLI command to fix it. Auth-requiring routes return 401 until provisioned; non-authenticated routes (`/`, `/health`, `/docs`, any host or plugin `authExcludedRoutes`) stay reachable.

## Per-request user auth

Two distinct concerns, two distinct artifacts — [`AuthHeaderMiddleware`](https://github.com/ixoworld/ixo-oracles-boilerplate/blob/main/packages/oracle-runtime/src/modules/auth/auth-header.middleware.ts) verifies both on every protected route:

* **Authentication (who is calling)** = a user-signed UCAN **invocation**. Short-lived, replay-bounded, sent as `Authorization: Bearer <invocation>` + `X-Auth-Type: ucan`. This is the **primary** auth path.
* **Authorization (what the oracle may do downstream)** = the user→oracle **delegation**, sent as `x-ucan-delegation`. Plugins read it to mint downstream-service invocations on the user's behalf.

<Note>
  A bare `x-ucan-delegation` (no invocation) is still accepted **as a migration fallback** for clients that haven't moved to invocation auth yet. New clients should send both: the invocation to authenticate, the delegation for downstream authorization.
</Note>

| Header                               | Required                    | Purpose                                                                                                                                                                    |
| ------------------------------------ | --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Authorization: Bearer <invocation>` | yes (primary)               | The user-signed UCAN invocation — proves *who* is calling. Only read when `X-Auth-Type: ucan` is also present.                                                             |
| `X-Auth-Type: ucan`                  | yes (primary)               | Selector that tells the middleware to read the bearer token as a UCAN invocation. Without it the bearer is ignored.                                                        |
| `x-ucan-delegation`                  | fallback / downstream-authz | The user→oracle delegation. Carries the capabilities plugins use to mint downstream invocations. Also accepted as the auth artifact on its own for pre-invocation clients. |
| `x-did`                              | no                          | The user's IXO DID (set by SDK; runtime derives the authenticated DID from the invocation/delegation regardless). Not used for authentication.                             |
| `x-matrix-access-token`              | no                          | For clients that already have a Matrix session.                                                                                                                            |
| `x-matrix-homeserver`                | no                          | Matrix homeserver for the user.                                                                                                                                            |
| `x-timezone`                         | no                          | Propagates to `rtCtx.user.timezone`.                                                                                                                                       |
| `x-request-id`                       | no                          | Correlation ID for logs and traces.                                                                                                                                        |

When neither an invocation nor a delegation is present, the middleware returns **401** with:

```text theme={"system"}
Missing UCAN authentication: provide Authorization: Bearer <invocation> with X-Auth-Type: ucan, or an x-ucan-delegation header
```

A malformed or expired invocation returns **401** `Invalid UCAN invocation`.

`AuthHeaderMiddleware`:

<Steps>
  <Step title="Validates the invocation (primary)">
    When `X-Auth-Type: ucan` + `Authorization: Bearer …` are present, [`validateUcanInvocation`](https://github.com/ixoworld/ixo-oracles-boilerplate/blob/main/packages/oracle-runtime/src/modules/auth/validate-ucan-invocation.ts) verifies the signature and audience (the oracle's `ORACLE_DID`) and rejects any invocation whose lifetime exceeds `UCAN_AUTH_MAX_TTL_SECONDS` (default 900s). Results are cached by the token's SHA-256 hash with **TTL = the invocation's own expiry**, so reusing the same token until it expires (JWT-style) doesn't re-hit Blocksync. The invocation's signer becomes `req.authData.did`.
  </Step>

  <Step title="Falls back to the delegation (migration only)">
    When no invocation is sent, [`validateUcanDelegation`](https://github.com/ixoworld/ixo-oracles-boilerplate/blob/main/packages/oracle-runtime/src/modules/auth/validate-ucan-delegation.ts) authenticates the request from `x-ucan-delegation` instead. Delegation results cache with TTL = the delegation's expiry (or a 3-minute fallback when it declares none).
  </Step>

  <Step title="Trusts the delegation downstream only when it's the caller's own">
    A delegation is public and shareable, so the middleware acts on it downstream **only when its issuer DID equals the authenticated DID**. When they differ, the delegation is ignored downstream — `req.authData.ucanDelegation.raw` is set to `''`, and plugins branch on `raw.length === 0`. This stops a client from pairing their own invocation with someone else's delegation to make the oracle act on that person's behalf.
  </Step>

  <Step title="Attaches RuntimeUserContext to the request">
    The next middleware (`RuntimeContextBuilder`) reads `req.authData` to build the `rtCtx.user` field handed to every tool handler — see the [Runtime context reference](/build-an-oracle/reference/runtime-context).
  </Step>
</Steps>

## Opt routes out of auth

Two mechanisms to expose public routes — host-level and plugin-level. Both merge onto the runtime's built-in exclusion list: `/` (the JSON landing payload), `/health`, `/docs`, and `/docs/(.*)`.

<Steps>
  <Step title="From the host — pass authExcludedRoutes to createOracleApp">
    ```ts theme={"system"}
    import { RequestMethod } from '@nestjs/common';
    import { createOracleApp, type AuthExcludedRoute } from '@ixo/oracle-runtime';

    const HOST_AUTH_EXCLUDED_ROUTES: AuthExcludedRoute[] = [
      { path: 'version', method: RequestMethod.GET },
    ];

    const app = await createOracleApp({
      config,
      nestModules: [VersionModule],
      authExcludedRoutes: HOST_AUTH_EXCLUDED_ROUTES,
    });
    ```

    Reference: [apps/qiforge-example/src/main.ts](https://github.com/ixoworld/ixo-oracles-boilerplate/blob/main/apps/qiforge-example/src/main.ts).
  </Step>

  <Step title="From a plugin — implement getAuthExcludedRoutes">
    ```ts theme={"system"}
    override getAuthExcludedRoutes(): AuthExcludedRoute[] {
      return [{ path: 'weather/now', method: RequestMethod.GET }];
    }
    ```

    Reference: [`weather.plugin.ts`](https://github.com/ixoworld/ixo-oracles-boilerplate/blob/main/apps/qiforge-example/src/plugins/weather/weather.plugin.ts).
  </Step>
</Steps>

Any other plugin or host route returns 401 without a UCAN header — exclusion is path-and-method specific.

## Mint downstream UCAN invocations

When a plugin calls a downstream service on the user's behalf, it does not reuse the user's invocation — it mints a fresh one signed by the oracle's `SECP_MNEMONIC`, derived from the user's stored delegation. The minted invocation goes out to the downstream service the same way the user authenticates to *this* oracle: `Authorization: Bearer <invocation>` + `X-Auth-Type: ucan`.

```ts theme={"system"}
handler: async (args, rtCtx: RuntimeContext) => {
  // No signing key (mnemonic not yet provisioned) → minting is a no-op.
  if (!rtCtx.ucan.hasSigningKey()) {
    return JSON.stringify({ error: 'Oracle signing key not provisioned.' });
  }

  // Resolve the service URL to its did:web identifier, then mint against it.
  const serviceDid = await rtCtx.ucan.resolveServiceDid(
    'https://downstream-service.example',
  );
  if (!serviceDid) {
    return JSON.stringify({ error: 'Could not resolve downstream service DID.' });
  }

  const invocation = await rtCtx.ucan.mintInvocation({
    did: serviceDid,
    capability: 'ixo:downstream',
  });

  const resp = await fetch('https://downstream-service.example/data', {
    headers: {
      Authorization: `Bearer ${invocation}`,
      'X-Auth-Type': 'ucan',
    },
  });
  // ...
}
```

The full UCAN helper surface on `rtCtx.ucan`:

| Method                                                               | Purpose                                                                                                                                                                   |
| -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `mintInvocation(target, opts?)`                                      | Mint a service-targeted invocation from the user's cached delegation. `target` = `{ did, capability }`; `opts.skipCache` forces a fresh signature.                        |
| `requireCapability(resource, action)`                                | Throws if the user's delegation doesn't include the capability.                                                                                                           |
| `hasCapability(resource, action)`                                    | Returns a boolean — non-throwing variant.                                                                                                                                 |
| `resolveServiceDid(serviceUrl)`                                      | Resolves a service URL to its `did:web:...` identifier. Returns `null` when the DID document is missing or has no `id`.                                                   |
| `hasSigningKey()`                                                    | Returns `true` once the oracle has loaded its Ed25519 signing mnemonic. Gate tool registration on this — without a key, minting is a no-op.                               |
| `createInvocationFromDelegation(car, serviceUrl, capability, opts?)` | Mint from a directly-supplied delegation CAR (rather than the per-user cached one). Returns `{ invocation }` or `{ error }`. Used by the editor's `mint_invocation` tool. |

See [`packages/oracle-runtime/src/modules/ucan/`](https://github.com/ixoworld/ixo-oracles-boilerplate/tree/main/packages/oracle-runtime/src/modules/ucan) for the service implementation.

## What plugins can and cannot do

* **Can:** read `rtCtx.user.did`, `rtCtx.user.matrixUserId`, `rtCtx.user.ucanDelegation`, `rtCtx.user.timezone`.
* **Can:** call `rtCtx.secrets.getIndex()` / `getValues()` to read per-room secrets.
* **Can:** mint downstream invocations via `rtCtx.ucan.mintInvocation`.
* **Cannot:** override the oracle's identity per request.
* **Cannot:** issue UCANs as anyone other than the oracle itself.

## Where to read next

<CardGroup cols={2}>
  <Card title="API endpoints" icon="server" href="/build-an-oracle/reference/api-endpoints">
    Which routes are auth-protected and which are public.
  </Card>

  <Card title="CLI reference" icon="terminal" href="/build-an-oracle/reference/cli">
    `create-entity`, `setup-encryption-key`, and the rest of identity setup.
  </Card>

  <Card title="Runtime context" icon="diagram-next" href="/build-an-oracle/reference/runtime-context">
    The full `rtCtx.user`, `rtCtx.ucan`, `rtCtx.secrets` surface.
  </Card>

  <Card title="Environment variables" icon="key" href="/build-an-oracle/reference/environment-variables">
    Every identity-related env var, declared and validated.
  </Card>
</CardGroup>
