# Authentication

Personal access tokens with scopes, AI agents as members of their own, and OAuth for apps that act for people.

Every `/v1` call sends a bearer token:

```http
GET /v1/spaces HTTP/1.1
Host: side.example.com
Authorization: Bearer ssp_6Jw…
```

There are three ways to get one, depending on who the program acts for.

| Program | Token | How |
|---|---|---|
| Your script, CI job or service | Personal access token `ssp_…` | `sidespace token create` |
| An AI agent or bot that should appear as itself | A token for an **agent member** | `sidespace agent create`, then `token create --agent` |
| An app that other people sign in to | OAuth 2.1 access token | Authorization code with PKCE |

People sign in to the apps with an email code, or through their organization's identity provider when it has set up [single sign-on](/docs/organizations#single-sign-on). A code works once. Sign-in endpoints answer `429` with code `rate_limited` and a `Retry-After` header (seconds) when an address or a network has tried too often; after 10 wrong codes in an hour the address is locked for the rest of that hour.

## Personal access tokens

Tokens start with `ssp_`. The server stores only a hash, so a token is shown once. Each token has scopes, can be limited to some spaces, and expires after 90 days unless you choose otherwise (`--expires-days 0` never expires).

```sh
sidespace token create "nightly export" --scope files:read --space "Film A"
sidespace token list
sidespace token revoke tok_01JD…
```

The CLI accepts a token too, with no sign-in: set `SIDESPACE_TOKEN=ssp_…` or pass `--token`.

### Scopes

| Scope | Allows |
|---|---|
| `files:read` | List spaces, browse, search, read versions and sidecars, download, read notifications |
| `files:write` | Everything in `files:read`, plus upload, create, rename, move, delete and restore |
| `sidecars:write` | Write any sidecar namespace |
| `sidecars:write:<ns>` | Write one namespace; a trailing `*` covers a family, e.g. `sidecars:write:x-acme.*` |
| `renditions:write` | Upload thumbnails, proxies and other renditions |
| `events:read` | Read the journal and realtime signals |
| `shares:manage` | Create and revoke share links |
| `members:manage` | Add, change and remove members |
| `webhooks:manage` | Manage the space's webhooks (the token's owner must be a space manager) |
| `runs:write` | Run actions such as transcribe, AI tags and convert (editor or above) |
| `pipelines:manage` | Manage pipelines (space manager) |
| `plugins:manage` | Install, update, turn off and remove plugins (space manager) |
| `comments:write` | Comment, reply and resolve (commenter or above) |
| `admin` | All of the above, plus creating and renaming spaces |

A scope never adds to what the person behind the token may do in a space: an `admin` token of a viewer is still a viewer there.

Tokens can't manage devices, tokens or agents. Those need a signed-in app or CLI, so a token can never mint a stronger token.

### Errors you will see

- `403` with `{"error": {"code": "forbidden", "message": "this token needs the files:write scope"}}` names the missing scope.
- `404` for a space outside the token's spaces, exactly like a space that doesn't exist. The API never confirms that a space you can't see exists.
- `402` with code `quota_exceeded` when a plan limit stops a save, a new member or an AI run. The message says which limit, on whose plan (a space counts toward its owner's), and when a monthly allowance renews. Show it as is. `GET /v1/me/plan` (or `sidespace plan`) returns your own plan, limits and use.

## Agents

An AI agent or service account can be a member of its own instead of borrowing a person's identity. Everything it changes is recorded with `actor_kind: "agent"`, and the apps show a purple **Agent** badge in members, version history and activity.

```sh
sidespace agent create "Clip Tagger"
sidespace members "Film A" --add "Clip Tagger" --role editor
sidespace token create tagger --agent "Clip Tagger" \
  --scope files:read --scope "sidecars:write:x-tagger.*"
sidespace agent rm "Clip Tagger"   # revokes its tokens and removes it from every space
```

Agents can't sign in with an email code; they act only through tokens. When an agent is removed, its past changes keep its name.

Work the server does itself is marked too. A thumbnail made by media processing is recorded as `actor_kind: "plugin"`, `actor_via: "media"`.

## OAuth for apps

Apps that act for other people (a web app, a desktop tool, an MCP client) use the OAuth 2.1 authorization code flow with PKCE, so nobody copies tokens around. Server metadata: `/.well-known/oauth-authorization-server`.

Register the app:

```sh
sidespace app create "Acme Review" --redirect https://acme.example/oauth/callback
# client_id = app_01…
sidespace app create "Acme Sync" --redirect https://acme.example/cb --confidential   # server-side app, also gets a client_secret
sidespace app list
sidespace app rm app_01…      # every grant stops working at once
```

Then:

1. Send the person to `/oauth/authorize` with `response_type=code`, `client_id`, `redirect_uri`, `state`, `code_challenge`, `code_challenge_method=S256` and `scope` (space-separated, e.g. `files:read files:write`).
2. They sign in with their email code, choose which scopes and spaces to grant, and are sent back to `redirect_uri?code=…&state=…&iss=…`.
3. Your app posts a form to `/oauth/token`: `grant_type=authorization_code`, `code`, `redirect_uri`, `client_id`, `code_verifier`. The answer has an `access_token` (`ssp_…`, one hour) and a `refresh_token` (`ssr_…`, 90 days, replaced at every refresh).
4. Refresh with `grant_type=refresh_token`. Sign out with `POST /oauth/revoke`.

```sh
curl -X POST https://side.example.com/oauth/token \
  -d grant_type=authorization_code -d code="$CODE" -d client_id=app_01… \
  -d redirect_uri=https://acme.example/oauth/callback -d code_verifier="$VERIFIER"
```

- Without `scope`, the default is `files:read files:write comments:write runs:write`. `admin` can't be granted to an app, and the person may untick scopes or pick only some spaces.
- A grant is a token: it appears in the person's `sidespace token list` as `app:…` and can be revoked there. Scopes, space limits and the 403/404 rules are the same as for any token.
- Redirect URIs must be https, loopback http (any port, RFC 8252) or the app's own scheme (`cursor://…`). Public clients must use PKCE.
- Apps may also register themselves at `POST /oauth/register` (RFC 7591, public clients only). MCP clients do this. The consent page says the app registered itself.
