# 认证

带权限范围的个人访问令牌、作为独立成员的 AI agent，以及代表用户操作的应用所用的 OAuth。

每个 `/v1` 调用都带一个 Bearer 令牌：

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

按程序代表谁操作，有三种拿令牌的方式。

| 程序 | 令牌 | 怎么拿 |
|---|---|---|
| 你的脚本、CI 任务或服务 | 个人访问令牌 `ssp_…` | `sidespace token create` |
| 应该以自己身份出现的 AI agent 或机器人 | **AI 成员**的令牌 | `sidespace agent create`，再 `token create --agent` |
| 给别人登录使用的应用 | OAuth 2.1 访问令牌 | 授权码流程加 PKCE |

人在应用里用邮箱验证码登录；组织设置了[单点登录](/docs/organizations#单点登录)时，用组织的身份提供方登录。验证码只能用一次。同一个邮箱或同一个网络尝试太多时，登录接口返回 `429`，代码 `rate_limited`，并带 `Retry-After` 头（秒）；一小时内输错 10 次验证码，这个邮箱在这一小时剩下的时间里都会被锁定。

## 个人访问令牌

令牌以 `ssp_` 开头。服务器只保存它的哈希，所以只在生成时显示一次。每个令牌都有权限范围（scope），可以限定只用于某几个空间，默认 90 天后过期（`--expires-days 0` 表示永不过期）。

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

CLI 也可以直接用令牌，不需要登录：设置 `SIDESPACE_TOKEN=ssp_…`，或者加 `--token` 参数。

### 权限范围

| 权限 | 允许做什么 |
|---|---|
| `files:read` | 列空间、浏览、搜索、读版本和附加信息、下载、看通知 |
| `files:write` | 在 `files:read` 之外，还能上传、新建、改名、移动、删除、恢复 |
| `sidecars:write` | 写任何命名空间的附加信息 |
| `sidecars:write:<ns>` | 只写这一个命名空间；以 `*` 结尾时包括它下面一组，例如 `sidecars:write:x-acme.*` |
| `renditions:write` | 上传缩略图、代理等派生文件 |
| `events:read` | 读变更日志和实时信号 |
| `shares:manage` | 新建、吊销分享链接 |
| `members:manage` | 添加、修改、移除成员 |
| `webhooks:manage` | 管理空间的 Webhook（令牌的主人还需要是空间管理员） |
| `runs:write` | 运行转写、AI 标签、转格式等动作（编辑者以上） |
| `pipelines:manage` | 管理流水线（空间管理员） |
| `plugins:manage` | 安装、更新、停用、卸载插件（空间管理员） |
| `comments:write` | 发表、回复、解决评论（评论者以上） |
| `admin` | 以上全部，另外还能新建、改名空间 |

权限范围不会超出令牌主人在空间里的角色：一个查看者生成的 `admin` 令牌，在那个空间里仍然只是查看者。

令牌做不了这几件事：管理设备、令牌和 AI 成员。它们只能在已登录的应用或 CLI 里做，所以令牌没法给自己生成一个权限更大的令牌。

### 会遇到的错误

- `403`，返回 `{"error": {"code": "forbidden", "message": "this token needs the files:write scope"}}`，写明缺哪个权限。
- `404`：访问令牌范围以外的空间时返回，和空间不存在时一模一样。API 不会透露你看不到的空间是否存在。
- `402`，代码 `quota_exceeded`：套餐限额挡住了一次保存、一次加成员或一次 AI 任务。消息里写明是哪项限额、算在谁的套餐上（空间算在所有者头上），按月的额度何时重置，原样显示即可。`GET /v1/me/plan`（或 `sidespace plan`）返回你自己的套餐、限额和用量。

## AI 成员

AI agent 或服务账号可以作为独立成员加入空间，而不是借用某个人的身份。它的每一次改动都记为 `actor_kind: "agent"`，应用里的成员列表、版本记录和动态会显示紫色的 **Agent** 标记。

```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"   # 吊销它的所有令牌，并把它移出所有空间
```

AI 成员不能用邮箱验证码登录，只能用令牌。移除后，它以前的改动仍然保留它的名字。

服务器自己的处理也会标明来源。比如媒体处理生成的缩略图，记为 `actor_kind: "plugin"`、`actor_via: "media"`。

## 应用接入（OAuth）

代表别人操作的应用（网页应用、桌面工具、MCP 客户端）用 OAuth 2.1 授权码流程加 PKCE，不需要让用户复制令牌。服务器元数据在 `/.well-known/oauth-authorization-server`。

注册应用：

```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   # 服务端应用，另给 client_secret
sidespace app list
sidespace app rm app_01…      # 删除后所有授权立即失效
```

然后：

1. 把用户带到 `/oauth/authorize`，带上 `response_type=code`、`client_id`、`redirect_uri`、`state`、`code_challenge`、`code_challenge_method=S256` 和 `scope`（空格分隔，例如 `files:read files:write`）。
2. 用户用邮箱验证码登录，勾选给哪些权限、哪些空间，然后回到 `redirect_uri?code=…&state=…&iss=…`。
3. 应用以表单 POST `/oauth/token`：`grant_type=authorization_code`、`code`、`redirect_uri`、`client_id`、`code_verifier`。返回 `access_token`（`ssp_…`，1 小时）和 `refresh_token`（`ssr_…`，90 天，每次续期都换新）。
4. 续期用 `grant_type=refresh_token`；退出时 `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"
```

- 不传 `scope` 时默认 `files:read files:write comments:write runs:write`。`admin` 不能授予应用，用户也可以去掉某些权限、只给某几个空间。
- 授权就是一个令牌：在用户的 `sidespace token list` 里显示为 `app:…`，可以单独吊销。权限、空间范围和 403/404 的规则与普通令牌完全一样。
- 回调地址必须是 https、本机的 http（端口任意，RFC 8252），或者应用自己的 scheme（`cursor://…`）。公开客户端必须用 PKCE。
- 应用也可以自行注册（`POST /oauth/register`，RFC 7591，只能是公开客户端），MCP 客户端就是这样接入的。同意页会提示这是自行注册的应用。
