# auth.md: UpHeron

UpHeron (https://upheron.com) monitors websites, APIs, certificates, DNS and mail servers from several locations. This file tells an AI agent how to get API access by itself, without a person creating an API token: on its own, or on behalf of its user. It follows the auth.md agent registration profile (https://workos.com/auth-md).

## 1. Discover

- `GET https://upheron.com/.well-known/oauth-protected-resource`: the API (`https://upheron.com/api/v1`), its scopes (`read`, `write`) and its authorization server, `https://upheron.com`.
- `GET https://upheron.com/.well-known/oauth-authorization-server`: the token and revocation endpoints, and an `agent_auth` block with the registration endpoint, the claim endpoint and `identity_types_supported`.
- An API request without credentials answers `401` with `WWW-Authenticate: Bearer resource_metadata="https://upheron.com/.well-known/oauth-protected-resource"`.

## 2. Choose how to register

| You have | `type` | What you get |
|---|---|---|
| Your user's email address | `service_auth` | Access to the user's workspace once they type a code you show them |
| An ID-JAG from your agent provider | `identity_assertion` | Access to the user's workspace at once if they linked your provider before; otherwise the same code step, once |
| Nothing: you act alone | `anonymous` | A pay-as-you-go agent workspace of your own, straight away. Your user can claim it later |

Acting for a person: use `service_auth` (or `identity_assertion` if your provider issues ID-JAGs and this service lists `identity_assertion` in `identity_types_supported`). Acting alone: use `anonymous`. An agent workspace has no free monitors; each monitor is paid per location ($0.01 per month, or $0.10 for life) with the Machine Payments Protocol, see https://upheron.com/docs/payments. Reading is free.

## 3. Register: `POST https://upheron.com/agent/identity`

JSON bodies:

```json
{ "type": "service_auth", "login_hint": "user@example.com" }
{ "type": "anonymous" }
{ "type": "identity_assertion", "assertion_type": "urn:ietf:params:oauth:token-type:id-jag", "assertion": "<ID-JAG>" }
```

The ID-JAG's `aud` must be `https://upheron.com`; it needs `email` with `email_verified: true` and an `auth_time` less than an hour old.

- **`anonymous`** answers `200` with `identity_assertion`, `claim_token` (keep it to let your user claim the workspace later; it works for 7 days) and `workspace`. Go to step 5.
- **`service_auth`** answers `200` with `claim_token` and a `claim` block, and no assertion yet. Go to step 4.
- **`identity_assertion`** answers `200` with `identity_assertion` when the provider identity is already linked; go to step 5. The first time it answers `401` with `"error": "interaction_required"`, a `claim_token` and a `claim` block; go to step 4.

## 4. Claim: the user types your code

Show your user `claim.verification_uri` and `claim.user_code`, for example: "Open <verification_uri>, sign in, and enter the code 123456." They sign in to UpHeron (or create an account), see what you are asking for, and type the code. Never send them the `claim_token`.

For an `anonymous` registration, start the ceremony when your user wants to take over your workspace. Their monitors, history and credit then move into the user's workspace:

```bash
curl -X POST https://upheron.com/agent/identity/claim -H "Content-Type: application/json" \
  -d '{"claim_token": "clm_…", "email": "user@example.com"}'
```

The answer has a `claim_attempt` block with a new `user_code` and `verification_uri`. Only the person signed in with that email can complete it. Call it again with the same `claim_token` and email to get a fresh code: a code works for 10 minutes and a link locks after 5 wrong codes.

Then poll every `interval` seconds (5):

```bash
curl -X POST https://upheron.com/oauth2/token \
  -d grant_type=urn:workos:agent-auth:grant-type:claim -d claim_token=clm_…
```

- `authorization_pending`: the user has not finished; keep polling.
- `slow_down`: you polled too fast; add 5 seconds to your interval.
- `expired_token`: the code or the claim window ran out. Restart the ceremony at `/agent/identity/claim`; if that answers `410 claim_expired`, register again.
- Success: a token response with `access_token`, plus `identity_assertion` and `assertion_expires`. Keep the assertion; the `claim_token` is spent.

## 5. Get an access token: `POST https://upheron.com/oauth2/token`

```bash
curl -X POST https://upheron.com/oauth2/token \
  -d grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer -d assertion=<identity_assertion>
```

```json
{ "access_token": "pga_…", "token_type": "Bearer", "expires_in": 3600, "scope": "read write",
  "identity_assertion": "<a fresh assertion>", "assertion_expires": "…" }
```

Access tokens last an hour. To refresh, exchange the identity assertion again. Each response carries a fresh assertion (valid 90 days); keep the newest one. `invalid_grant` means the assertion expired or the user disconnected you: register again.

## 6. Use the token

- API: `https://upheron.com/api/v1` with `Authorization: Bearer pga_…`. For requests that may be paid, send `X-API-Key: pga_…` instead, because an MPP client puts its payment credential in `Authorization`. Docs: https://upheron.com/docs/api
- MCP server: `https://upheron.com/mcp` with the same header.
- Scopes: `read` (every GET) and `write` (create, change, delete, pay). The token acts for one workspace.

On a `401` from a token that worked, exchange the assertion once more; if that fails, register again.

## 7. Revoke

```bash
curl -X POST https://upheron.com/oauth2/revoke -d token=pga_…
```

Always answers `200`. Your user can disconnect you in Settings → Connected agents, which ends your tokens and assertion at once.

## Errors

| `error` | Where | What to do |
|---|---|---|
| `invalid_request` | any | Fix the request; `error_description` says what is wrong |
| `invalid_login_hint` | `/agent/identity` | Give a valid email in `login_hint` |
| `rate_limited` (429) | `/agent/identity` | Wait `Retry-After` seconds |
| `invalid_issuer`, `invalid_signature`, `invalid_audience`, `expired`, `replay_detected`, `missing_verified_email` | `/agent/identity` | Mint a fresh, correct ID-JAG; an unknown issuer cannot be used here |
| `login_required` (401) | `/agent/identity` | Have the user sign in again at your provider, then mint a fresh ID-JAG |
| `interaction_required` (401) | `/agent/identity` | Run the claim ceremony (step 4) |
| `invalid_claim_token` (401), `claim_expired` (410) | `/agent/identity/claim` | Register again |
| `claimed_or_in_flight` (409) | `/agent/identity/claim` | Already claimed: poll the token endpoint |
| `invalid_grant`, `unsupported_grant_type` | `/oauth2/token` | See step 5 |

## Rules

- Before anything that costs money (`402 Payment Required`), tell your user the amount and ask, unless they approved a budget.
- Keep `identity_assertion`, `claim_token` and access tokens secret. Never put them in prompts or logs.
- Only ask a user to type a code for a ceremony you started for them.
