# Authorize: getting a token

An agent or a CLI gets its scoped capability token through the platform's
browser loop (tosijs-platform#6): the client starts a request with a
*label* (virta's is machine × repo) and the *caveats* it wants (roles,
collections, methods), a human approves it in a browser, and the client
exchanges a proof for the token. This module is that protocol, ported from
the platform's reference script, with no I/O beyond `fetch` so it runs in
Bun, Node, or a browser. The loopback listener and the token file live in
`bin/login.ts`, which uses it.

Three things matter, and they are the same three the reference names:

1. **The verifier never leaves the process.** Only its SHA-256 is sent
   (PKCE). A consent URL, a browser history entry, or a stolen request id is
   useless on its own.
2. **The token arrives only at the exchange**, over TLS, to whoever holds
   the verifier. It never passes through the browser.
3. **Loopback mode binds 127.0.0.1** and checks the request id coming back
   is the one it started, so a phished approval redirects nowhere useful.
   *Poll* mode has no such binding — the warning it prints is not
   decorative: only approve a URL you generated, on a machine you control.

```
POST /authorize?action=start    { label, caveats, codeChallenge, mode, redirectPort?, ttlMs? }
                                → { status: 'started', requestId, consentUrl, expiresAt, pollIntervalMs }
GET  /authorize?request=<id>    the consent page (a human, signed in)
POST /authorize?action=exchange { requestId, verifier }
                                → { status: 'pending', pollIntervalMs } | { status: 'ready', id, secret, label, caveats, expiresAt }
                                | { status: 'denied' } | { status: 'refused', … }
```

The token itself is `tsp_…`, sent as `Authorization: Bearer`. It attenuates
the approver: effective rights are their live roles ∩ the caveats, it cannot
mint another token, and it lives at most ninety days.
