# Client API v0.1 — предложенный контракт

**Статус: PROPOSED_DOCUMENT_ONLY.** [OpenAPI 3.1](client-api.openapi.json) — материал для реализации и ревью. Работающий backend, клиент KMP, выпуск токенов и сетевые проверки здесь не заявлены. Он независим от MCU KQIP 0.2: телефон передаёт намерение в Core, а не подписывает произвольную команду MCU.

## Основной поток

1. В foreground приложение получает `GET /v0/installations/{installation_id}/snapshot`. Snapshot и opaque cursor относятся к одной атомарной границе журнала.
2. Приложение открывает `/events?after=cursor` обычным потоковым HTTP-клиентом с `Authorization` в заголовке. SSE `id` равен `Event.cursor`; `data` — JSON `Event`. Порядок определяется append position, а не временем измерения. Cursor сохраняется после применения события. Сервер обеспечивает непрерывный переход replay→live.
3. HTTP 410 `RESYNC_REQUIRED` до открытия потока означает новый snapshot. Разрыв истории или смена прав уже внутри SSE передаёт соответствующее событие и закрывает поток; клиент также получает новый snapshot. Cursor нельзя конструировать, переносить между пользователями, установками или поколениями авторизации. При сетевом разрыве сначала восстанавливается поток, а не повторяется управление.
4. Явное действие пользователя создаёт UUID `Idempotency-Key` и фиксированное тело `UserIntent`: target, SET/RELEASE_MANUAL, значение и исходный snapshot/boot/fence/manual/output revisions. Core сам определяет principal, права, cause, execution_seq и MCU deadline. Эти поля не принимаются из клиентского JSON; snapshot не является полномочием.
5. `202 RECEIVED` выдается только после commit намерения, idempotency record и outbox. Совпавший ключ и каноническое тело в области principal×installation возвращают тот же intent; изменённое тело — 409. Проверка текущих прав предшествует возврату истории и повторяется перед dispatch. Потеря HTTP-ответа не создаёт нового намерения.

## История и физический результат

| Статус | Достаточное основание | Не означает |
|---|---|---|
| RECEIVED | Намерение долговечно принято | Команда уже отправлена или выполнена |
| REJECTED | Отказ до dispatch либо подтверждённый отказ MCU | Что другое действие не изменило устройство |
| EXECUTED | Аутентифицированное известное MCU APPLIED | Физический свет/контакт измерен |
| UNKNOWN | Отправка/действие возможны, результата недостаточно | Команда не выполнялась |

`EffectEvidence` отдельно описывает NOT_CHECKED / OBSERVED / INCONCLUSIVE / MISMATCH и ссылки на свидетельства. Исторический EXECUTED не переписывается из нового snapshot. UNKNOWN может разрешиться после сверки, но не запускает новую команду автоматически. Перед socket write нужен durable DeliveryAttempt; crash injection — открытая приёмка backend.

Минимальные 30 дней idempotency retention — **предложенный предел этого draft**, а не обещание реализованного сервиса. Сервер сообщает границу заголовком `Idempotency-Retention-Until`. После неё автоматический повтор запрещён: новый snapshot, объяснение неизвестного прошлого результата и новое явное намерение пользователя. Бессрочное exactly-once не обещается. Capacity/retention policy необходимо утвердить до реализации. Числовые uint64 передаются десятичными строками; runtime дополнительно проверяет максимум `18446744073709551615`.

## KMP, foreground и отзыв ключа

Общая KMP-логика может хранить intent ID, исходное неизменяемое тело и cursor; фоновые iOS/Android ограничения не заменяются предположением о постоянном SSE. После foreground выполняется проверка действующей сессии, затем resync/reconnect. Локальное отображение помечает stale/unavailable и не выдаёт старую кнопку/ревизию за актуальное разрешение. Ключи и session credentials хранятся платформенным безопасным механизмом выбранного профиля; конкретная аппаратная защита не утверждается.

После commit отзыва пользовательской сессии Core закрывает подписки, отменяет неотправленное и повторяет права перед dispatch. Старое поколение авторизации инвалидирует cursor, idempotent history access и будущие запросы. Клиент очищает защищённый кэш и ожидающие управленческие действия, прекращает reconnect со старым credential. Уже отправленное действие ограничено исходным сроком; гарантированная отмена по разорванной сети не обещается.

User session, MCU TLS peer и owner/recovery authority — разные сущности. Этот API не задаёт issuance/recovery/OwnerSetUpdate. Отзыв сессии телефона не отзывает автоматически owner-подписанта на MCU; неподтвердившие обновление узлы остаются PENDING. Протокол выпуска, ротации, хранения и отзыва credentials остаётся gate.

## Приёмка и незакрытые границы

`validate_client_api.py` проверяет структуру документа, локальные `$ref`, корректность JSON Schema и положительные/отрицательные intent/event examples. Это проверка договора, не сервера. Запуск: `python3 validate_client_api.py`; нужен пакет `jsonschema`.

До API freeze нужны реализация и отрицательные tests: подмена installation/principal, revoke до cache и перед dispatch, concurrent idempotency commit, changed-body retry, crashes до/после attempt/write, snapshot→stream без потерь, foreign/expired cursor, revoke в открытом SSE, восстановление KMP после suspend, собственные timeout/backpressure/retention лимиты. Предлагаемые статусы и коды не являются новыми значениями KQIP.
