Document client API contract

This commit is contained in:
delete 2026-06-11 00:27:12 +03:00
parent 71598cda7d
commit a90dd9bc50
2 changed files with 632 additions and 0 deletions

View file

@ -248,6 +248,7 @@ environment variables.
## More Docs
- `docs/API.md` - current client API contract, examples, responses, and errors.
- `docs/DEPLOYMENT.md` - deployment, reverse proxy, backup, and restore notes.
- `deploy/update-server.env.example` - production environment example.
- `deploy/Caddyfile.example` - Caddy reverse proxy example.

631
docs/API.md Normal file
View file

@ -0,0 +1,631 @@
# Update Server API
Этот документ описывает текущий публичный HTTP API сервиса. Его можно передать
клиентскому приложению или агенту, который встраивает автообновление.
Примеры ниже используют:
```text
Base URL: https://updates.example.com
API key: upsk_replace_me
Project: desktop-app
```
В реальной интеграции замени `Base URL`, API key и `project_slug` на свои
значения.
## Общая модель
Update Server хранит проекты и релизы:
- `project` - приложение, продукт или канал обновлений;
- `release` - загруженный файл обновления внутри проекта;
- `api key` - клиентский ключ доступа к API.
Администратор работает через web UI:
```text
/admin
/admin/projects
/admin/api-keys
```
Клиентские приложения работают через JSON API:
```text
/api/v1
```
На текущий момент JSON API предназначен для чтения и скачивания обновлений.
Создание проектов, загрузка релизов и создание API keys выполняются через admin
web UI, а не через публичный JSON API.
## Авторизация
Защищенные endpoint'ы требуют bearer token:
```http
Authorization: Bearer <api_key>
```
Пример:
```bash
curl -i \
-H "Authorization: Bearer upsk_replace_me" \
https://updates.example.com/api/v1/projects
```
API key должен быть:
- активным;
- не истекшим по `expires_at`, если срок задан;
- с разрешением `can_download`;
- со scope, который разрешает доступ к нужному проекту.
Поддерживаемые scope modes:
```text
all_projects
project_allow_list
project_deny_list
tag_allow_list
tag_deny_list
```
Полный API key показывается в admin UI только один раз при создании. Сервер
хранит только hash ключа.
## Формат JSON
Все JSON-ответы возвращаются с:
```http
Content-Type: application/json; charset=utf-8
```
Поля времени возвращаются строкой в формате RFC3339/RFC3339Nano, например:
```json
"created_at": "2026-06-11T10:15:30Z"
```
Относительные URL в ответах, например `download_url`, нужно склеивать с
`Base URL` клиента.
## Health Check
### GET /healthz
Публичная проверка готовности HTTP-сервера и SQLite.
```bash
curl -i https://updates.example.com/healthz
```
Успешный ответ:
```http
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
```
```json
{
"database": "ok",
"service": "Update Server",
"status": "ok",
"timestamp": "2026-06-11T10:15:30Z"
}
```
Если база недоступна:
```http
HTTP/1.1 503 Service Unavailable
```
```json
{
"database": "unavailable",
"error": "database ping failed",
"service": "Update Server",
"status": "degraded",
"timestamp": "2026-06-11T10:15:30Z"
}
```
## API Index
### GET /api/v1
Публичный JSON-index текущей версии API. Авторизация не нужна.
```bash
curl -i https://updates.example.com/api/v1
```
Ответ:
```json
{
"auth": {
"header": "Authorization: Bearer <api_key>",
"type": "bearer"
},
"routes": [
{
"description": "List active projects accessible to the API key.",
"method": "GET",
"path": "/api/v1/projects"
},
{
"description": "Get the latest active release metadata for an accessible project.",
"method": "GET",
"path": "/api/v1/projects/{projectSlug}/releases/latest"
},
{
"description": "Get release metadata for an accessible release.",
"method": "GET",
"path": "/api/v1/releases/{releaseID}"
},
{
"description": "Download the private artifact for an accessible release.",
"method": "GET",
"path": "/api/v1/releases/{releaseID}/download"
}
],
"service": "Update Server",
"status": "client-api-ready",
"version": "v1"
}
```
## List Accessible Projects
### GET /api/v1/projects
Возвращает активные проекты, доступные текущему API key.
Требует:
```http
Authorization: Bearer <api_key>
```
Пример:
```bash
curl -i \
-H "Authorization: Bearer upsk_replace_me" \
https://updates.example.com/api/v1/projects
```
Успешный ответ:
```json
{
"projects": [
{
"id": 1,
"name": "Desktop App",
"slug": "desktop-app",
"description": "Primary desktop updater stream.",
"latest_release_url": "/api/v1/projects/desktop-app/releases/latest"
}
]
}
```
Если доступных проектов нет:
```json
{
"projects": []
}
```
## Get Latest Release
### GET /api/v1/projects/{projectSlug}/releases/latest
Возвращает latest active release для активного и доступного проекта.
Latest release выбирается по:
```text
created_at DESC, id DESC
```
Пример:
```bash
curl -i \
-H "Authorization: Bearer upsk_replace_me" \
https://updates.example.com/api/v1/projects/desktop-app/releases/latest
```
Успешный ответ:
```json
{
"project": {
"id": 1,
"name": "Desktop App",
"slug": "desktop-app",
"description": "Primary desktop updater stream.",
"latest_release_url": "/api/v1/projects/desktop-app/releases/latest"
},
"release": {
"id": 2,
"version": "1.1.0",
"build": "build-2",
"filename": "desktop-app-1.1.0.zip",
"checksum_sha256": "4bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7c6e64b2f7110b2",
"size_bytes": 12345678,
"content_type": "application/zip",
"release_notes": "Improved desktop rollout.",
"created_at": "2026-06-11T10:15:30Z",
"metadata_url": "/api/v1/releases/2",
"download_url": "/api/v1/releases/2/download"
}
}
```
Если проект не существует, архивирован или недоступен этому key:
```http
HTTP/1.1 404 Not Found
```
```json
{
"error": "resource not found"
}
```
Если проект доступен, но активных релизов нет:
```http
HTTP/1.1 404 Not Found
```
```json
{
"error": "release not found"
}
```
## Get Release Metadata
### GET /api/v1/releases/{releaseID}
Возвращает metadata релиза по ID. Релиз должен быть активным, его проект должен
быть активным, и текущий API key должен иметь доступ к проекту релиза.
Пример:
```bash
curl -i \
-H "Authorization: Bearer upsk_replace_me" \
https://updates.example.com/api/v1/releases/2
```
Успешный ответ:
```json
{
"project": {
"id": 1,
"name": "Desktop App",
"slug": "desktop-app",
"description": "Primary desktop updater stream.",
"latest_release_url": "/api/v1/projects/desktop-app/releases/latest"
},
"release": {
"id": 2,
"version": "1.1.0",
"build": "build-2",
"filename": "desktop-app-1.1.0.zip",
"checksum_sha256": "4bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7c6e64b2f7110b2",
"size_bytes": 12345678,
"content_type": "application/zip",
"release_notes": "Improved desktop rollout.",
"created_at": "2026-06-11T10:15:30Z",
"metadata_url": "/api/v1/releases/2",
"download_url": "/api/v1/releases/2/download"
}
}
```
Если `releaseID` не число:
```http
HTTP/1.1 400 Bad Request
```
```json
{
"error": "invalid release id"
}
```
Если релиз не существует, архивирован или недоступен:
```http
HTTP/1.1 404 Not Found
```
```json
{
"error": "resource not found"
}
```
## Download Release Artifact
### GET /api/v1/releases/{releaseID}/download
Скачивает приватный artifact релиза. Скачивание проходит через приложение, чтобы
сервер мог проверить API key, permission и project scope.
Пример:
```bash
curl -L \
-H "Authorization: Bearer upsk_replace_me" \
-o desktop-app-1.1.0.zip \
https://updates.example.com/api/v1/releases/2/download
```
Успешный ответ:
```http
HTTP/1.1 200 OK
Content-Disposition: attachment; filename=desktop-app-1.1.0.zip
Content-Type: application/zip
X-Content-Type-Options: nosniff
```
Body ответа - bytes загруженного файла.
Если artifact отсутствует на диске:
```http
HTTP/1.1 404 Not Found
```
```json
{
"error": "release not found"
}
```
Если релиз недоступен:
```http
HTTP/1.1 404 Not Found
```
```json
{
"error": "resource not found"
}
```
## Project Object
```json
{
"id": 1,
"name": "Desktop App",
"slug": "desktop-app",
"description": "Primary desktop updater stream.",
"latest_release_url": "/api/v1/projects/desktop-app/releases/latest"
}
```
Поля:
- `id` - numeric project ID.
- `name` - display name.
- `slug` - stable URL identifier.
- `description` - optional description.
- `latest_release_url` - relative URL для latest release lookup.
## Release Object
```json
{
"id": 2,
"version": "1.1.0",
"build": "build-2",
"filename": "desktop-app-1.1.0.zip",
"checksum_sha256": "4bf5122f344554c53bde2ebb8cd2b7e3d1600ad631c385a5d7c6e64b2f7110b2",
"size_bytes": 12345678,
"content_type": "application/zip",
"release_notes": "Improved desktop rollout.",
"created_at": "2026-06-11T10:15:30Z",
"metadata_url": "/api/v1/releases/2",
"download_url": "/api/v1/releases/2/download"
}
```
Поля:
- `id` - numeric release ID.
- `version` - версия, введенная администратором при загрузке.
- `build` - build label или build number, если задан.
- `filename` - download filename.
- `checksum_sha256` - SHA-256 загруженного artifact.
- `size_bytes` - размер artifact в bytes.
- `content_type` - MIME type artifact.
- `release_notes` - release notes.
- `created_at` - время загрузки релиза.
- `metadata_url` - relative URL metadata endpoint.
- `download_url` - relative URL download endpoint.
## Ошибки
JSON API возвращает ошибки в формате:
```json
{
"error": "message"
}
```
Основные статусы:
| Status | Когда |
| --- | --- |
| `400` | Некорректный `releaseID`, например не число. |
| `401` | Нет bearer token, token неверный, key выключен или истек. |
| `403` | API key валиден, но нет `can_download`. |
| `404` | Ресурс не найден, архивирован или недоступен текущему key. |
| `429` | Сработал rate limit client API. |
| `500` | Внутренняя ошибка сервера. |
| `503` | Health check: база недоступна; или API key service недоступен. |
### 401 Unauthorized
Missing header:
```http
WWW-Authenticate: Bearer realm="update-server"
```
```json
{
"error": "missing bearer api key"
}
```
Invalid, disabled or expired key:
```json
{
"error": "invalid api key"
}
```
### 403 Forbidden
```json
{
"error": "api key permission denied"
}
```
### 429 Too Many Requests
```http
Retry-After: 3
```
```json
{
"error": "rate limit exceeded"
}
```
По умолчанию client API rate limit настраивается переменными:
```text
APP_CLIENT_RATE_LIMIT_PER_MINUTE=120
APP_CLIENT_RATE_LIMIT_BURST=60
```
## Headers
Для protected API responses сервер добавляет:
```http
Cache-Control: no-store, private, max-age=0
Pragma: no-cache
Expires: 0
Vary: Authorization
X-Robots-Tag: noindex, nofollow
```
Также на все ответы добавляются security headers, включая:
```http
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Referrer-Policy: no-referrer
```
Если `APP_BASE_URL` использует `https://`, включается:
```http
Strict-Transport-Security: max-age=31536000
```
## Типовой Client Flow
1. Администратор создает проект в `/admin/projects`.
2. Администратор загружает release artifact в проекте.
3. Администратор создает API key в `/admin/api-keys`.
4. Для ключа включает `can_download`.
5. Для ключа выбирает scope, который разрешает доступ к проекту.
6. Клиент запрашивает latest release:
```bash
curl -sS \
-H "Authorization: Bearer upsk_replace_me" \
https://updates.example.com/api/v1/projects/desktop-app/releases/latest
```
7. Клиент сравнивает `release.version` со своей текущей версией.
8. Если версия новая, клиент скачивает `release.download_url`.
9. Клиент проверяет:
- `checksum_sha256`;
- `size_bytes`;
- при необходимости свою подпись artifact, если она есть в продукте.
## Python Example
В репозитории есть минимальный helper:
```text
example/autoupdate.py
example/update_client.py
```
Приватный файл рядом со скриптом:
```json
{
"base_url": "https://updates.example.com",
"api_key": "upsk_replace_me",
"project_slug": "desktop-app"
}
```
Вызов:
```python
from autoupdate import ensure_updated_from_config
ver = "1.0.0"
ensure_updated_from_config(current_version=ver)
```
Если `autoupdate.json` отсутствует, helper создаст файл с placeholder values,
не пойдет в сеть и выведет короткое сообщение о настройке конфига.
## Чего Сейчас Нет В API
На текущий момент client JSON API не предоставляет:
- создание/редактирование проектов;
- загрузку релизов;
- создание/редактирование API keys;
- список всех релизов проекта;
- release channels вроде `stable` / `beta`;
- query parameters вроде `platform`, `arch`, `current_version`;
- публичные downloads без API key.
Эти действия либо выполняются через admin web UI, либо пока являются будущим
расширением API.