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>
This commit is contained in:
1 parent
10272f3851
commit
dfb4fe4e1a
17 files changed
+2496
-56
No files matched your search
+73
-4
@@ -82,14 +82,21 @@ POST /v1/sync
|
||||
}
|
||||
```
|
||||
|
||||
Поддерживаемые entity:
|
||||
Поддерживаемые entity (контракт v2, `docs/CONTRACT_V2.md` §1):
|
||||
|
||||
```text
|
||||
transaction
|
||||
category
|
||||
budget
|
||||
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 отправляет локальные изменения.
|
||||
@@ -112,6 +119,68 @@ Server возвращает изменения после этого cursor, в
|
||||
|
||||
Если существует 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`.
|
||||
|
||||
## Ошибки
|
||||
|
||||
Рекомендуемый контракт:
|
||||
|
||||
Reference in new issue
Block a user