diff --git a/README.md b/README.md index 9b45847..30e59d5 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/docs/API.md b/docs/API.md new file mode 100644 index 0000000..2371cd3 --- /dev/null +++ b/docs/API.md @@ -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 +``` + +Пример: + +```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 ", + "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 +``` + +Пример: + +```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.