# Authentication

Mana is an end-user iOS product. This document explains how authentication
works and what is — and isn't — available to automated agents.

## Discover

Agents should start with these discovery documents:

- `https://mana.am/llms.txt`
- `https://mana.am/openapi.json`
- `https://mana.am/.well-known/oauth-protected-resource`
- `https://mana.am/.well-known/oauth-authorization-server`

The public read API is available without credentials. Protected app endpoints
are reserved for the Mana iOS app.

## How people sign in

Authentication happens **inside the Mana iOS app** (iOS 26+). Supported
sign-in methods:

- **Sign in with Apple**
- **Google**
- **Email code** (one-time code sent to your email address)

After sign-in, the app holds a short-lived access token and a longer-lived
refresh token used only by the app to talk to the private backend at
`https://api.mana.am`. These tokens are issued for the app's own use; they are
not part of a public, agent-facing credential program.

## Pick a method

- Public discovery: use no auth. Send normal HTTPS `GET` requests to the
  documented `/public/*` endpoints.
- End-user product use: ask the user to open the Mana iOS app and sign in with
  Apple, Google, or email code.
- Agent automation: no public credential method is available for creating,
  editing, publishing, or managing creations.
- Metadata cross-check: `identity_types_supported` is `["user"]`, and
  `anonymous.credential_types_supported`,
  `identity_assertion.assertion_types_supported`,
  `identity_assertion.credential_types_supported`, and `events_supported` are
  empty arrays. Agents should stop instead of attempting an unsupported
  registration shape.

## Register

Mana does not currently support third-party OAuth client registration,
dynamic client registration, or machine-to-machine credentials.

`agent_auth` metadata is published only to make this boundary machine-readable:

```json
{
  "agent_auth": {
    "status": "not_available",
    "register_uri": "https://mana.am/agent/auth/register",
    "claim_uri": "https://mana.am/agent/auth/claim",
    "revocation_uri": "https://mana.am/agent/auth/revoke",
    "identity_types_supported": ["user"],
    "email": {
      "status": "app_only",
      "enabled_for_agents": false,
      "claim_uri": "https://mana.am/agent/auth/claim",
      "credential_types_supported": [],
      "template": {
        "identity_type": "user",
        "delivery": "email_code",
        "code_format": "numeric",
        "code_length": 6,
        "recipient_field": "email",
        "public_agent_flow": false,
        "user_action": "Open the Mana iOS app, choose Email, enter the one-time code there, and continue in the app."
      },
      "endpoint_signature": {
        "method": "POST",
        "content_type": "application/json",
        "example_body": {
          "method": "email_code",
          "email": "user@example.com",
          "code": "123456",
          "claim_token": "claim_token_from_register_uri"
        },
        "example_post_body": {
          "method": "email_code",
          "email": "user@example.com",
          "code": "123456",
          "claim_token": "claim_token_from_register_uri"
        },
        "fields": {
          "method": "Email-code claim method. Always email_code for this template.",
          "email": "End-user email address. Agents must not collect it for Mana unless Mana later enables public agent auth.",
          "code": "Six-digit numeric one-time code entered in the Mana iOS app, not in chat.",
          "claim_token": "Opaque token returned by register_uri if a future public agent flow is enabled."
        }
      },
      "method": "POST",
      "content_type": "application/json",
      "example_post_body": {
        "method": "email_code",
        "email": "user@example.com",
        "code": "123456",
        "claim_token": "claim_token_from_register_uri"
      },
      "fields": {
        "method": {
          "type": "string",
          "required": true,
          "description": "Email-code claim method. Always email_code for this template."
        },
        "email": {
          "type": "string",
          "format": "email",
          "required": true,
          "description": "End-user email address. Public agents must not collect it for Mana unless Mana later enables public agent auth."
        },
        "code": {
          "type": "string",
          "pattern": "^[0-9]{6}$",
          "required": true,
          "description": "Six-digit numeric one-time code entered in the Mana iOS app, not in chat."
        },
        "claim_token": {
          "type": "string",
          "required": true,
          "description": "Opaque token returned by register_uri if a future public agent flow is enabled."
        }
      },
      "unsupported_error": "email_not_enabled"
    },
    "skill": "https://mana.am/auth.md",
    "skill_uri": "https://mana.am/.well-known/agent-skills/mana/SKILL.md",
    "anonymous": { "credential_types_supported": [] },
    "identity_assertion": {
      "assertion_types_supported": [],
      "credential_types_supported": []
    },
    "events_supported": [],
    "documentation_uri": "https://mana.am/auth.md"
  }
}
```

These URIs resolve so agents can discover the boundary, but they return
`agent_auth_not_available` instead of issuing credentials.

### Email-code template

Mana supports email-code sign-in only inside the Mana iOS app. The template is
published so agents can parse the boundary instead of inventing an OTP flow:

```json
{
  "email": {
    "status": "app_only",
    "enabled_for_agents": false,
    "claim_uri": "https://mana.am/agent/auth/claim",
    "credential_types_supported": [],
    "template": {
      "identity_type": "user",
      "delivery": "email_code",
      "code_format": "numeric",
      "code_length": 6,
      "recipient_field": "email",
      "public_agent_flow": false,
      "user_action": "Open the Mana iOS app, choose Email, enter the one-time code there, and continue in the app."
    },
    "endpoint_signature": {
      "method": "POST",
      "content_type": "application/json",
      "example_body": {
        "method": "email_code",
        "email": "user@example.com",
        "code": "123456",
        "claim_token": "claim_token_from_register_uri"
      },
      "example_post_body": {
        "method": "email_code",
        "email": "user@example.com",
        "code": "123456",
        "claim_token": "claim_token_from_register_uri"
      },
      "fields": {
        "method": "Email-code claim method. Always email_code for this template.",
        "email": "End-user email address. Agents must not collect it for Mana unless Mana later enables public agent auth.",
        "code": "Six-digit numeric one-time code entered in the Mana iOS app, not in chat.",
        "claim_token": "Opaque token returned by register_uri if a future public agent flow is enabled."
      }
    },
    "method": "POST",
    "content_type": "application/json",
    "example_post_body": {
      "method": "email_code",
      "email": "user@example.com",
      "code": "123456",
      "claim_token": "claim_token_from_register_uri"
    },
    "fields": {
      "method": {
        "type": "string",
        "required": true,
        "description": "Email-code claim method. Always email_code for this template."
      },
      "email": {
        "type": "string",
        "format": "email",
        "required": true,
        "description": "End-user email address. Public agents must not collect it for Mana unless Mana later enables public agent auth."
      },
      "code": {
        "type": "string",
        "pattern": "^[0-9]{6}$",
        "required": true,
        "description": "Six-digit numeric one-time code entered in the Mana iOS app, not in chat."
      },
      "claim_token": {
        "type": "string",
        "required": true,
        "description": "Opaque token returned by register_uri if a future public agent flow is enabled."
      }
    },
    "unsupported_error": "email_not_enabled"
  }
}
```

Agents must not ask a user to paste a Mana email code into chat. If the user
wants to sign in with email, send them to the app. Public agent calls to the
email claim endpoint return `email_not_enabled` or `agent_auth_not_available`.

### Example POST body

Example POST body for the email-only authentication template, documented for
parsers only:

```http
POST /agent/auth/claim
Content-Type: application/json
```

```json
{
  "method": "email_code",
  "email": "user@example.com",
  "code": "123456",
  "claim_token": "claim_token_from_register_uri"
}
```

Field documentation:

| Field         | Type   | Required if enabled | Meaning                                                                    |
| ------------- | ------ | ------------------- | -------------------------------------------------------------------------- |
| `method`      | string | yes                 | Email-code claim method. Use `email_code`.                                 |
| `email`       | string | yes                 | End-user email address. Public agents must not collect it for Mana today.  |
| `code`        | string | yes                 | Six-digit numeric one-time code. It belongs in the Mana iOS app, not chat. |
| `claim_token` | string | yes                 | Opaque token from `register_uri` if a future public flow is enabled.       |

## Claim

Mana does not issue an agent credential through `identity_assertion`, `id-jag`,
JWT bearer assertion, or any other claim flow. If a user wants Mana to build or
modify something, direct them to the iOS app.

## Use the credential

For public data, no credential is needed:

```bash
curl "https://api.mana.am/public/share/community?sort=recent&limit=5"
```

For protected app endpoints, the Mana iOS app sends an app-issued bearer token.
Agents must not ask users to paste private app tokens.

Unauthenticated probes to protected discovery paths return
`WWW-Authenticate: Bearer resource_metadata="https://mana.am/.well-known/oauth-protected-resource"`.

## Errors

Mana does not issue public agent credentials, so registration, claim, and
revocation endpoints return `agent_auth_not_available`. If Mana later enables
agent credentials, agents should use this canonical error table:

Canonical parser table for the 12 WorkOS auth.md agent error codes:

| Error code                         | Endpoint       | Meaning                                                 | Agent action                                             |
| ---------------------------------- | -------------- | ------------------------------------------------------- | -------------------------------------------------------- |
| `invalid_issuer`                   | `register_uri` | Assertion issuer is not trusted.                        | Stop and rediscover trusted provider support.            |
| `invalid_signature`                | `register_uri` | Assertion signature verification failed.                | Stop, refresh the assertion, and retry once.             |
| `expired`                          | `register_uri` | Assertion or flow token is expired.                     | Start again with a fresh token.                          |
| `replay_detected`                  | `register_uri` | Assertion `jti` or nonce was already used.              | Generate a fresh assertion and nonce.                    |
| `invalid_audience`                 | `register_uri` | Assertion audience does not match Mana's auth server.   | Request an assertion for `https://mana.am`.              |
| `invalid_client_id`                | `register_uri` | Agent provider client ID is unknown.                    | Stop and use a known provider identity.                  |
| `missing_verified_email`           | `register_uri` | Assertion lacks a verified email or phone claim.        | Ask the user to verify identity in the agent provider.   |
| `unsupported_credential_type`      | `register_uri` | Requested credential type is not supported.             | Choose only credential types listed in metadata.         |
| `insufficient_user_authentication` | `register_uri` | Provider authentication context does not meet policy.   | Ask the user to re-authenticate with stronger assurance. |
| `invalid_claim_token`              | `claim_uri`    | Claim token is malformed, unknown, or invalid.          | Restart registration if claim is supported.              |
| `claimed_or_in_flight`             | `claim_uri`    | Claim is already complete or currently being completed. | Wait briefly, then inspect current registration state.   |
| `claim_expired`                    | `claim_uri`    | Claim token has expired.                                | Restart registration.                                    |

Additional compatible auth error codes Mana documents for future-proofing:

| Error code                       | Endpoint          | Meaning                                      | Agent action                                               |
| -------------------------------- | ----------------- | -------------------------------------------- | ---------------------------------------------------------- |
| `audience_mismatch`              | `register_uri`    | Assertion audience is not a Mana audience.   | Request an assertion for `https://mana.am`.                |
| `credential_expired`             | protected API     | Agent credential is expired.                 | Rediscover metadata and restart registration if supported. |
| `anonymous_not_enabled`          | `register_uri`    | Anonymous registration is disabled.          | Do not retry anonymously.                                  |
| `identity_assertion_not_enabled` | `register_uri`    | Identity assertion registration is disabled. | Pick another supported method or stop.                     |
| `email_not_enabled`              | `claim_uri`       | Email claim flow is disabled.                | Do not ask the user for an OTP.                            |
| `rate_limited`                   | any auth endpoint | Too many attempts.                           | Wait for `Retry-After` or `RateLimit-Reset`.               |
| `otp_invalid`                    | `claim_uri`       | OTP is incorrect.                            | Ask the user to re-enter it; do not guess repeatedly.      |
| `otp_expired`                    | `claim_uri`       | OTP has expired.                             | Restart the claim ceremony.                                |
| `previously_claimed`             | `claim_uri`       | Claim was already completed.                 | Use the issued credential if available; otherwise restart. |

Auth.md canonical WorkOS agent error codes:

- Error code `invalid_issuer`: assertion issuer is not trusted.
- Error code `invalid_signature`: JWKS lookup or signature verification failed.
- Error code `expired`: assertion or flow token is expired.
- Error code `replay_detected`: assertion `jti`, nonce, or logout token was
  already used.
- Error code `invalid_audience`: assertion audience does not match Mana's auth
  server.
- Error code `invalid_client_id`: `client_id` does not resolve to a known agent
  provider identity.
- Error code `missing_verified_email`: assertion lacks a verified email or
  phone claim needed to match a user.
- Error code `unsupported_credential_type`: requested credential type is not
  offered by the service.
- Error code `insufficient_user_authentication`: the agent provider auth context
  did not meet policy.
- Error code `invalid_claim_token`: claim token is malformed, unknown, or not
  valid for this environment.
- Error code `claimed_or_in_flight`: claim was already claimed or is currently
  being completed.
- Error code `claim_expired`: claim token expired before completion.
- Error code `otp_invalid`: OTP is incorrect.
- Error code `otp_expired`: OTP has expired.
- Error code `previously_claimed`: claim was already completed.

Required auth.md agent-registration error coverage:

- Error code `invalid_issuer`: assertion issuer is not trusted.
- Error code `invalid_signature`: signature verification failed.
- Error code `replay_detected`: nonce or assertion was already used.
- Error code `invalid_audience`: assertion audience is wrong.
- Error code `audience_mismatch`: assertion audience is wrong.
- Error code `expired`: assertion or credential is expired.
- Error code `credential_expired`: credential is expired.
- Error code `invalid_client_id`: agent provider client ID is unknown.
- Error code `missing_verified_email`: no verified user identity is available
  for matching.
- Error code `insufficient_user_authentication`: user authentication context did
  not meet policy.
- Error code `anonymous_not_enabled`: anonymous registration is disabled.
- Error code `identity_assertion_not_enabled`: identity assertion registration
  is disabled.
- Error code `email_not_enabled`: email claim flow is disabled.
- Error code `unsupported_credential_type`: requested credential type is not
  supported.
- Error code `rate_limited`: wait for `Retry-After` or `RateLimit-Reset`.
- Error code `invalid_claim_token`: claim token is malformed or unknown.
- Error code `claimed_or_in_flight`: claim is already complete or currently in
  progress.
- Error code `otp_invalid`: OTP is incorrect.
- Error code `otp_expired`: OTP has expired.
- Error code `claim_expired`: claim token has expired.
- Error code `previously_claimed`: claim was already completed.

OAuth-compatible fallback error codes:

- Error code `invalid_request`: request is malformed or missing a required
  parameter.
- Error code `invalid_client`: client authentication failed or the client is
  unknown.
- Error code `invalid_grant`: grant, assertion, OTP, or claim token is invalid.
- Error code `unauthorized_client`: this client is not allowed to use the
  requested auth method.
- Error code `unsupported_grant_type`: requested grant type is not supported.
- Error code `unsupported_response_type`: requested response type is not
  supported.
- Error code `invalid_scope`: requested scope is unknown, malformed, or not
  allowed.
- Error code `access_denied`: resource owner or server denied the request.
- Error code `server_error`: authorization server hit an unexpected error.
- Error code `temporarily_unavailable`: authorization server is temporarily
  unavailable.
- Error code `interaction_required`: user interaction is required before the
  flow can continue.
- Error code `login_required`: the user must sign in before the flow can
  continue.
- Error code `consent_required`: explicit user consent is required before the
  flow can continue.
- Error code `account_selection_required`: the user must select an account
  before the flow can continue.

| Code                               | Endpoint                                      | Meaning                                                                   | Agent action                                               |
| ---------------------------------- | --------------------------------------------- | ------------------------------------------------------------------------- | ---------------------------------------------------------- |
| `agent_auth_not_available`         | `register_uri`, `claim_uri`, `revocation_uri` | Mana has no public agent credential flow.                                 | Use public read APIs or direct the user to the iOS app.    |
| `invalid_issuer`                   | `register_uri`, `revocation_uri`              | Assertion issuer is not trusted.                                          | Stop and rediscover trusted provider support.              |
| `invalid_request`                  | any auth endpoint                             | Request is malformed or missing a required parameter.                     | Fix the request shape before retrying.                     |
| `invalid_client`                   | `register_uri`                                | Client authentication failed or the client is unknown.                    | Rediscover metadata and stop if no client exists.          |
| `invalid_client_id`                | `register_uri`                                | Agent provider client ID is unknown.                                      | Stop and use a known provider identity.                    |
| `invalid_grant`                    | `claim_uri`                                   | Grant, assertion, OTP, or claim token is invalid.                         | Restart the supported flow, if any.                        |
| `unauthorized_client`              | any auth endpoint                             | Client is not allowed to use the requested auth method.                   | Stop or pick a supported method.                           |
| `unsupported_grant_type`           | `claim_uri`                                   | Requested grant type is not supported.                                    | Use only grant types listed in metadata.                   |
| `unsupported_response_type`        | `register_uri`                                | Requested response type is not supported.                                 | Use only response types listed in metadata.                |
| `invalid_scope`                    | any auth endpoint                             | Requested scope is unknown, malformed, or not allowed.                    | Reduce to documented scopes.                               |
| `access_denied`                    | any auth endpoint                             | User or server denied the request.                                        | Stop and explain the denial to the user.                   |
| `server_error`                     | any auth endpoint                             | Authorization server hit an unexpected error.                             | Retry with backoff and check status.                       |
| `temporarily_unavailable`          | any auth endpoint                             | Authorization server is temporarily unavailable.                          | Retry later with backoff.                                  |
| `expired`                          | `register_uri`, `claim_uri`                   | Assertion or flow token is expired.                                       | Start the flow again with a fresh token.                   |
| `interaction_required`             | `claim_uri`                                   | User interaction is required.                                             | Send the user to the Mana iOS app.                         |
| `login_required`                   | `claim_uri`                                   | User must sign in first.                                                  | Ask the user to open Mana and sign in.                     |
| `consent_required`                 | `claim_uri`                                   | User consent is required.                                                 | Ask the user to complete consent in the app.               |
| `account_selection_required`       | `claim_uri`                                   | User must select an account.                                              | Ask the user to choose an account in the app.              |
| `invalid_signature`                | `register_uri`, `revocation_uri`              | Identity assertion or logout token signature could not be verified.       | Stop, refresh the assertion, and retry once.               |
| `replay_detected`                  | `register_uri`                                | The assertion or nonce was already used.                                  | Generate a fresh assertion and nonce.                      |
| `invalid_audience`                 | `register_uri`                                | The assertion audience is not `https://mana.am` or `https://api.mana.am`. | Request an assertion for the correct audience.             |
| `audience_mismatch`                | `register_uri`                                | The assertion audience is not `https://mana.am` or `https://api.mana.am`. | Request an assertion for the correct audience.             |
| `credential_expired`               | protected API                                 | The credential is expired.                                                | Rediscover metadata and restart registration if supported. |
| `missing_verified_email`           | `register_uri`                                | The assertion lacks a verified email or phone claim.                      | Ask the user to verify identity in the agent provider.     |
| `insufficient_user_authentication` | `register_uri`                                | The provider auth context does not meet policy.                           | Ask the user to re-authenticate with stronger assurance.   |
| `anonymous_not_enabled`            | `register_uri`                                | Anonymous agent credentials are not enabled.                              | Do not retry anonymously.                                  |
| `identity_assertion_not_enabled`   | `register_uri`                                | Identity assertions are not accepted.                                     | Pick another supported method or stop.                     |
| `email_not_enabled`                | `register_uri`                                | Email claim flow is not enabled.                                          | Do not ask the user for an OTP.                            |
| `unsupported_credential_type`      | `register_uri`                                | Requested `api_key` or `access_token` type is unsupported.                | Choose a credential type listed in metadata.               |
| `rate_limited`                     | any agent auth endpoint                       | Too many attempts.                                                        | Wait for `Retry-After` or `RateLimit-Reset`.               |
| `invalid_claim_token`              | `claim_uri`                                   | Claim token is malformed or unknown.                                      | Restart registration if claim is supported.                |
| `claimed_or_in_flight`             | `claim_uri`                                   | Claim is already complete or currently being completed.                   | Wait briefly, then inspect current registration state.     |
| `otp_invalid`                      | `claim_uri`                                   | OTP was incorrect.                                                        | Ask the user to re-enter it; do not guess repeatedly.      |
| `otp_expired`                      | `claim_uri`                                   | OTP expired.                                                              | Restart the claim ceremony.                                |
| `claim_expired`                    | `claim_uri`                                   | The claim token expired.                                                  | Restart registration.                                      |
| `previously_claimed`               | `claim_uri`                                   | Claim was already completed.                                              | Use the issued credential if available; otherwise restart. |

HTTP-level errors:

- `401 Unauthorized`: a protected app endpoint was called without a valid app
  bearer token. Use the public API or direct the user to the iOS app.
- `403 Forbidden`: the current app session is not allowed to perform the
  requested action.
- `404 Not Found`: no endpoint exists at this path. Fetch
  `https://mana.am/openapi.json` and choose a documented public endpoint.
- `429 Too Many Requests`: wait for `Retry-After` or the `RateLimit-Reset`
  window before retrying.
- `5xx`: retry with exponential backoff and check `https://mana.am/status`.

Canonical auth.md error-code coverage, as plain text for parsers:
`invalid_issuer`, `invalid_signature`, `expired`, `replay_detected`,
`invalid_audience`, `invalid_client_id`, `missing_verified_email`,
`unsupported_credential_type`, `insufficient_user_authentication`,
`invalid_claim_token`, `claimed_or_in_flight`, `claim_expired`,
`otp_invalid`, `otp_expired`, `previously_claimed`, `invalid_request`,
`invalid_client`, `invalid_grant`,
`unauthorized_client`, `unsupported_grant_type`,
`unsupported_response_type`, `invalid_scope`, `access_denied`,
`server_error`, `temporarily_unavailable`, `interaction_required`,
`login_required`, `consent_required`, `account_selection_required`,
`audience_mismatch`, `credential_expired`, `anonymous_not_enabled`,
`identity_assertion_not_enabled`, `email_not_enabled`,
and `rate_limited`.

Machine-readable error inventory:

```json
{
  "errors_supported": [
    "invalid_request",
    "invalid_issuer",
    "invalid_client",
    "invalid_client_id",
    "invalid_grant",
    "unauthorized_client",
    "unsupported_grant_type",
    "unsupported_response_type",
    "invalid_scope",
    "access_denied",
    "expired",
    "server_error",
    "temporarily_unavailable",
    "interaction_required",
    "login_required",
    "consent_required",
    "account_selection_required",
    "invalid_signature",
    "replay_detected",
    "invalid_audience",
    "audience_mismatch",
    "credential_expired",
    "missing_verified_email",
    "insufficient_user_authentication",
    "anonymous_not_enabled",
    "identity_assertion_not_enabled",
    "email_not_enabled",
    "unsupported_credential_type",
    "rate_limited",
    "invalid_claim_token",
    "claimed_or_in_flight",
    "otp_invalid",
    "otp_expired",
    "claim_expired",
    "previously_claimed"
  ]
}
```

## Revocation

End users revoke app sessions by signing out in Mana or by revoking Apple /
Google account access. There is no public `revocation_uri` for agents because
there are no public agent credentials; `https://mana.am/agent/auth/revoke`
returns `agent_auth_not_available` for discovery compatibility.

## What agents can use

- **Public read API (no credentials).** The endpoints documented in the
  [OpenAPI spec](https://mana.am/openapi.json) are public and unauthenticated.
  They expose the same public data the website renders — the community feed,
  popular tags, public creator profiles, and public app share pages. Base URL:
  `https://api.mana.am`. Just send a normal HTTPS GET request; no token, no
  OAuth handshake.

- **Agent auth skill metadata.** `agent_auth.skill` points to
  `https://mana.am/auth.md`, matching the WorkOS auth.md discovery examples.
  The optional `skill_uri` points to the separate Agent Skills artifact at
  `https://mana.am/.well-known/agent-skills/mana/SKILL.md`.

- **Authenticated / write endpoints.** Creating, editing, publishing, or
  managing creations requires an authenticated session and happens through the
  iOS app. There is **no** public client-registration flow, **no** OAuth
  authorization server for third parties, and **no** machine-to-machine
  credential issuance. Requests to protected endpoints without a valid bearer
  token return `401 Unauthorized` with a `WWW-Authenticate: Bearer` header.

## Summary

| Surface                       | Auth                        | Who                      |
| ----------------------------- | --------------------------- | ------------------------ |
| Public read API (`/public/*`) | None                        | Anyone, including agents |
| App build / write API         | App-issued bearer token     | The Mana iOS app only    |
| Account sign-in               | Apple / Google / email code | End users, in-app        |

If you are an agent helping a user, point them to the
[App Store](https://apps.apple.com/app/id6757949329) to get Mana, and use the
public read API for discovery. Questions: support@mana.am
