Files
BalanceServer/docs/API.md
T
wtandClaude Opus 5.5 dfb4fe4e1a Implement ecosystem contract v2: capabilities, shared ledgers, attachments
- GET /v1/capabilities and the extended sync entity allowlist (§0, §1).
- Shared ledgers (§7): ledgers, members and hashed one-time invite codes;
  X-Balance-Ledger selects the space on /v1/sync and /v1/attachments with
  its own records, change stream and cursor. Personal space keeps the v1
  tables and behaviour; the migration only adds tables.
- Attachments (§8): SHA-256, size and media type checks, blob files in
  BALANCE_ATTACHMENTS_DIR (default: attachments next to the database),
  garbage collection of unreferenced blobs with a grace period for fresh
  uploads, and ledger deletion removes its blobs.
- Tests for every new endpoint and error code; API, sync, data model,
  security, deployment and README docs updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 23:30:12 +07:00

221 lines
8.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# HTTP API
## Общие правила
API предназначен для iOS/macOS/watchOS и Android Balance.
Базовый URL задаётся клиентом и зависит от deployment environment.
JSON:
```http
Content-Type: application/json
```
Защищённые endpoints используют:
```http
Authorization: Bearer <access-token>
```
## Authentication
Основные операции:
```text
POST /v1/auth/register
POST /v1/auth/login
POST /v1/auth/logout
POST /v1/auth/refresh
```
### Register
Создаёт пользовательский аккаунт.
Input должен валидироваться сервером. Password никогда не хранится plaintext.
### Login
Проверяет credentials и создаёт authenticated session.
### Refresh
Принимает refresh credential и выдаёт новый access credential согласно текущей server-side session policy.
### Logout
Инвалидирует текущую session/refresh state согласно текущей реализации.
## Synchronization
Основной endpoint:
```text
POST /v1/sync
```
Логический request:
```json
{
"cursor": 123,
"deviceId": "UUID",
"changes": [],
"pull": true
}
```
Точная JSON schema должна оставаться совместимой с `ServerSync.swift` и Android `SyncRepository`.
## Change
Логическая форма:
```json
{
"entity": "transaction",
"id": "UUID",
"deleted": false,
"updatedAt": "2026-09-08T10:00:00Z",
"payload": {}
}
```
Поддерживаемые entity (контракт v2, `docs/CONTRACT_V2.md` §1):
```text
transaction category budget account transfer recurring
goal debt rate setting attachment
```
Незнакомая entity отклоняется `400 invalid_change`; текст сообщения
(`entity must be transaction, category, or budget`) сохранён из v1, потому что
он записан в эталоне `tools/compatibility_expected.txt`. Payload сервер хранит
как есть и не валидирует по сущности.
Заголовок `X-Balance-Ledger: <id>` выбирает общее пространство (см. ниже). Без
заголовка (или с пустым значением) — личное пространство, как в v1.
## Push
Client отправляет локальные изменения.
Server должен:
1. аутентифицировать пользователя;
2. проверить ownership/ID;
3. валидировать payload;
4. применить изменения;
5. создать server change entries;
6. вернуть результат и актуальную sync metadata.
Операция должна быть безопасна при retry.
## Pull
Client передаёт cursor.
Server возвращает изменения после этого cursor, включая deletions.
Если существует pagination, ответ должен содержать понятный признак продолжения и новый cursor/sequence.
## Контракт v2
### Capabilities
`GET /v1/capabilities` — без авторизации:
```json
{"contract":2,
"entities":["transaction","category","budget","account","transfer","recurring","goal","debt","rate","setting","attachment"],
"attachments":{"maxBytes":10485760,"types":["image/jpeg","image/png","image/heic","application/pdf"]},
"ledgers":true}
```
### Общие пространства (ledgers)
Все запросы — с `Authorization`. Объект ledger:
`{"id","name","role","members":[{"userId","email","role"}]}`, `role` —
роль текущего пользователя (`owner` | `member`), владелец идёт первым в `members`.
| Метод | Путь | Успех | Кто |
| --- | --- | --- | --- |
| GET | `/v1/ledgers` | `200 {"ledgers":[…]}` (только общие) | любой |
| POST | `/v1/ledgers` `{"name"}` | `201` ledger, `role:"owner"` | любой |
| PATCH | `/v1/ledgers/{id}` `{"name"}` | `200` ledger | владелец |
| DELETE | `/v1/ledgers/{id}` | `204` | владелец; удаляет записи, курсор, приглашения, участников и файлы вложений |
| POST | `/v1/ledgers/{id}/invites` | `201 {"code","expiresAt"}` | владелец |
| POST | `/v1/ledgers/join` `{"code"}` | `200` ledger, `role:"member"` | любой |
| DELETE | `/v1/ledgers/{id}/members/{userId}` | `204` | владелец — любого участника; участник — себя |
- Имя: после `trim` 1–100 символов без управляющих → иначе `400 invalid_name`.
- Код приглашения: 10 символов из `ABCDEFGHJKLMNPQRSTUVWXYZ23456789`, живёт 7 дней,
одноразовый. При вводе регистр, пробелы и `-` игнорируются. Повторный join
участника возвращает ledger и код не расходует. `/v1/ledgers/join` входит в
общий лимит 30 запросов/мин на IP вместе с auth endpoints.
- Ошибки: `400 invalid_code` (неверный, использованный или просроченный код),
`403 forbidden` (не участник или не владелец), `404 not_found` (нет такого
пространства или участника), `409 owner_cannot_leave` (владелец пытается
выйти — он может только удалить пространство), `400 invalid_json`.
- `X-Balance-Ledger` на `/v1/sync` и `/v1/attachments/*`: не участник → `403
forbidden`, несуществующее → `404 not_found`.
### Вложения
| Метод | Путь | Ответ |
| --- | --- | --- |
| PUT | `/v1/attachments/{sha256}` | тело — байты; `201` первый раз, `200` повтор; `{"sha256","size","contentType"}` |
| GET/HEAD | `/v1/attachments/{sha256}` | `200`, байты, сохранённый `Content-Type`, `Content-Disposition: attachment`; `404 not_found`, если в пространстве нет файла |
- `{sha256}` — 64 hex-символа в нижнем регистре; SHA-256 тела должен совпасть,
иначе `400 hash_mismatch` (так же для неверного формата хеша в PUT).
- `Content-Type` (параметры игнорируются) — `image/jpeg`, `image/png`,
`image/heic`, `application/pdf`, иначе `415 unsupported_media_type`.
- Больше 10 MiB → `413 payload_too_large`. Лимит тела 10 MiB действует только для
авторизованного PUT; остальные запросы по-прежнему ограничены 2 MiB.
### CORS
Ответы v1 сохраняют заголовки эталона Go. Для маршрутов v2 и для настоящих
preflight-запросов (есть `Access-Control-Request-Method`) разрешённые origin
получают `Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS`
и `Access-Control-Allow-Headers: Authorization, Content-Type, X-Balance-Ledger`.
## Ошибки
Рекомендуемый контракт:
```json
{
"error": {
"code": "invalid_request",
"message": "Human-readable safe message"
}
}
```
Не возвращать:
- SQL error;
- stack trace;
- password/token;
- filesystem paths;
- внутренние identifiers инфраструктуры.
## HTTP status semantics
Рекомендуемая семантика:
```text
200 OK — успешный request
201 Created — создание ресурса
400 Bad Request — malformed/invalid input
401 Unauthorized — missing/expired auth
403 Forbidden — resource belongs to another user
404 Not Found — resource/endpoint unavailable
409 Conflict — semantic conflict, если endpoint использует такую семантику
429 Too Many Requests — rate limit
500 Internal Server Error — internal failure
```
Конкретные status codes текущей реализации имеют приоритет над этой общей рекомендацией.