# auth.md

You are an agent. Box supports agentic registration.

Use the **same host you fetched this file from** for every URL. Staging, city CLIs, and production all serve this file; do not hardcode ascii.dev.

Authoritative endpoints live in:

```http
GET /.well-known/oauth-protected-resource
GET /.well-known/oauth-authorization-server
```

Those also exist under `/api/box/.well-known/...`. Read `issuer`, `token_endpoint`, `revocation_endpoint`, `grant_types_supported`, and the `agent_auth` block. Then follow the steps in order.

Box does not accept ID-JAG identity assertions. Use `anonymous` to explore, or `service_auth` when you have the user's email. Creating boxes and starting the trial require a completed claim plus Stripe Checkout (card on file, same fraud controls as a human signup).

## Expect a human in the loop

Nearly every step below sends a credential or an email address to a host you have no prior relationship with, and agent harnesses gate exactly that. Expect to be stopped and asked for approval more than once — registration and the first authenticated call are the usual points.

That is the flow working, not failing. When it happens:

- Say what you are about to send and to whom, then wait for the person.
- Resume from the step you were on. Every step here is independently retryable, so a pause costs nothing.
- Do not try to route around the gate.

The claim token and the identity assertion both outlive a pause — check the `claim_token_expires` and `assertion_expires` fields you were given. Only the 6-digit `user_code` is short-lived, so if it lapses, get a fresh one from Step 4a rather than hurrying the person.

## Step 1 — Discover

A 401 from the Box API carries:

```http
WWW-Authenticate: Bearer resource_metadata="<origin>/api/box/.well-known/oauth-protected-resource"
```

If you don't have the 401, fetch the well-known paths above.

`identity_types_supported` is `["anonymous","service_auth"]`.

## Step 2 — Pick a method

1. You have the user's email → `service_auth`. Claim ceremony required before any credential.
2. You have neither a user session nor an email → `anonymous`. You get a pre-claim token immediately (`box.me box.limits`). Claim later to unlock `box.me box.limits box.write box.billing` and start the trial.

Do not send `identity_assertion`. Box returns `issuer_not_enabled`.

Before `service_auth`, confirm with the user: you will register them with **Box** and they will sign in to bind this agent.

## Step 3 — Register

`POST /api/box/agent/identity` (also `POST /agent/identity`).

### service_auth

```http
POST /api/box/agent/identity
Content-Type: application/json

{"type":"service_auth","login_hint":"user@example.com"}
```

200 body includes `claim_token` (hold in memory) and a `claim` block (`user_code`, `verification_uri`, `expires_in`, `interval`). No `identity_assertion` yet. Go to Step 4.

### anonymous

```http
POST /api/box/agent/identity
Content-Type: application/json

{"type":"anonymous"}
```

200 body includes `identity_assertion`, `pre_claim_scopes`, `claim_token`, and `post_claim_scopes`. Exchange the assertion (Step 5) to call `GET /me` and `GET /limits`. To let a human take ownership and start a trial, go to Step 4.

## Step 4 — Claim ceremony

The user types a 6-digit `user_code` **you give them** into a page Box owns. The code does not come back to you.

### 4a. Get the ceremony materials

`service_auth`: already in the Step 3 `claim` block.

`anonymous`:

```http
POST /api/box/agent/identity/claim
Content-Type: application/json

{"claim_token":"clm_...","email":"user@example.com"}
```

### 4b. Hand off to the user

Send the link on its own first. Say what it is and that a code follows — a bare URL with no sentence around it leaves the person guessing:

> Open this link and sign in to Box — GitHub, Google, or email. Tell me once you're in and I'll send you a code to enter.
> `<origin>/api/box/agent/identity/claim?claim_attempt_token=...`

Only once they tell you they are signed in, send the code:

> Now enter this agent code: **123456**

Send both, in that order, and keep the explanatory wording. The one thing to leave out is why you are splitting them, which is your business rather than theirs. (Why: signing in by email issues its own 6-digit code, so anyone holding both at once can put one in the wrong field.) The agent code goes into the Box page after sign-in — it never comes back to you.

### 4c. Poll for completion

```http
POST /api/box/oauth2/token
Content-Type: application/x-www-form-urlencoded

grant_type=urn:workos:agent-auth:grant-type:claim
&claim_token=<clm_...>
```

While waiting: `{"error":"authorization_pending"}`. Honor `interval` (5s). On `slow_down`, add 5s. On `expired_token`, re-call Step 4a. On success you get `access_token` plus a new `identity_assertion`. Drop any pre-claim token. Polling again returns the same live token; it does not mint another key.

## Step 5 — Exchange the assertion

Use this to get the first pre-claim token, or to rotate a live token (the previous key is revoked).

```http
POST /api/box/oauth2/token
Content-Type: application/x-www-form-urlencoded

grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer
&assertion=<identity_assertion>
&resource=<origin>/api/box/v1/
```

Response: `access_token` (a Box API key, `box_...`), `token_type=Bearer`, `expires_in`, `scope`.

## Step 6 — Use the access_token

```http
GET /api/box/v1/me
Authorization: Bearer <access_token>
```

Pre-claim tokens may only call `GET /me` and `GET /limits`. Everything else returns `claim_required`.

When the token stops working, re-run Step 5 with the current assertion. If that returns `invalid_grant`, restart at Step 3.

API reference: https://docs.box.ascii.dev/box/api/v1

## Step 7 — Start the free trial

Claimed tokens can start Box's 7-day Base-plan trial. This is the same checkout a human uses: Stripe collects a card, existing trial-abuse checks still apply, and a second trial is never issued.

**Never quote a price from memory or from a web search.** `GET /api/box/v1/limits` returns the current plans, what the trial includes, and what it caps — read the numbers there, at the time you need them. If the person wants to estimate what their usage will cost, send them to <https://box.ascii.dev>, which has the calculator. Search results and cached marketing pages are not a price source — some of them describe other Ascii products entirely.

```http
POST /api/box/billing/checkout
Authorization: Bearer <access_token>
Content-Type: application/json

{"kind":"subscription"}
```

Surface the returned `url` to the user. Poll `GET /api/box/v1/limits` until `canStart` is true, then create a box:

```http
POST /api/box/v1/boxes
Authorization: Bearer <access_token>
Content-Type: application/json

{"ttlSeconds":3600}
```

If checkout returns `trial_blocked`, stop and tell the user to contact support. Do not try to open another account.

## Errors

| Code | Where | What to do |
| --- | --- | --- |
| `anonymous_not_enabled` | `/agent/identity` | Use `service_auth`. |
| `service_auth_not_enabled` | `/agent/identity` | Use `anonymous`. |
| `issuer_not_enabled` | `/agent/identity` | ID-JAG is not accepted. Use `service_auth` or `anonymous`. |
| `invalid_request` | `/agent/identity` | Fix the body. |
| `invalid_claim_token` | `/agent/identity/claim` | Restart at Step 3. |
| `claimed_or_in_flight` | `/agent/identity/claim` | Already claimed, or wrong endpoint. |
| `claim_expired` | `/agent/identity/claim` | Restart at Step 3. |
| `invalid_grant` | `/oauth2/token` | Restart at Step 3. |
| `unsupported_grant_type` | `/oauth2/token` | Use jwt-bearer or the agent-auth claim grant. |
| `authorization_pending` | `/oauth2/token` | Wait `interval` seconds and retry. |
| `expired_token` | `/oauth2/token` | Re-call `/agent/identity/claim`. |
| `slow_down` | `/oauth2/token` | Add 5s to `interval`. |
| `rate_limited` | any | Back off. |
| `claim_required` | Box API | Finish Step 4 before this call. |
| `billing_required` | Box API | Finish Step 7. |

## Revocation

POST `token=<access_token>&token_type_hint=access_token` to `/api/box/oauth2/revoke`. 200, idempotent. The `identity_assertion` still works; re-run Step 5 (that rotates — the revoked key stays dead).
