# ADR-041: Долг по ключу комнаты — состояние, а не событие

Уточняет [ADR-018](018-rooms-membership-rekey.md): «шлёт остальным событие `room` с `needsRekey: true`» дополняется признаком, который событие переживает.

## Контекст

ADR-018 описывает окно без rekey как временное: «Пока владелец офлайн, комната живёт на старом ключе — вышедший его и так знает». Значит, вернувшись, владелец обязан ключ сменить.

Вернуть его было нечем. `needsRekey` жил только в живом событии `room`: `GET /api/rooms` этого признака не нёс вовсе, в очередь событие не кладётся, а клиентский долг держался в памяти вкладки и умирал от перезагрузки. Владелец, не подключённый в ту секунду, когда участник вышел, не узнавал о долге никогда, и комната оставалась на ключе, который унёс вышедший, — до следующей смены состава, то есть, возможно, навсегда. Перезагрузка страницы у подключённого владельца давала то же самое, вместе с полосой «нужен новый ключ комнаты», которая исчезала молча.

`docs/protocol.md` при этом утверждает: «room и room_left в очередь не кладутся: клиент после каждого `ready` перечитывает `GET /api/rooms`… поэтому пропуск события во время офлайна ничего не ломает». Для `needsRekey` утверждение было ложным.

Тот же провал на втором пути. `DELETE /api/me` уносит участника из всех его комнат, но не шлёт оставшимся ничего: ни `room` с `needsRekey`, ни уведомления новому владельцу. По составу это тот же выход участника, что и `POST /api/rooms/{id}/leave`, и ADR-018 требует того же rekey. Хуже: `room_keys.sender` внешним ключом не защищён, поэтому текущий ключ комнаты остаётся завёрнутым исчезнувшим ником, и новое устройство оставшегося участника получить ключ уже не может.

Третье место — собственный выход. `POST /api/rooms/{id}/leave` шлёт `room` оставшимся и ничего — другим устройствам вышедшего. Они держат комнату в списке до следующего `ready`, то есть часами: при живом потоке `ready` не наступает.

## Решение

- `rooms` получает колонку `needs_rekey` (миграция 002). Ставится в 1, когда состав уменьшился, а участники остались: выход участника и удаление аккаунта. Снимается в 0 при `POST /api/rooms/{id}/members` — любая смена состава раздаёт новый ключ всему итоговому составу.
- `GET /api/rooms` отдаёт признак полем `needsRekey`. Клиент-владелец поднимает долг из списка комнат так же, как из события, и потому переживает и офлайн, и перезагрузку вкладки.
- `DELETE /api/me` — выход из всех комнат пользователя: оставшимся уходит `event: room` с `needsRekey: true` и их собственным текущим ключом, владение и пустые комнаты обрабатываются как при выходе (ADR-018).
- `POST /api/rooms/{id}/leave` шлёт `event: room_left` устройствам вышедшего, кроме отправившего запрос: их состояние сходится сразу, а не к следующему `ready`.
- Правятся `docs/protocol.md` («Типы», «Комнаты», «Аккаунт», «События») и `docs/storage.md` (миграция 002).

## Следствия

- Утверждение протокола про пропуск событий во время офлайна становится верным: всё, что несёт событие `room`, есть и в `GET /api/rooms`.
- Комната не остаётся на ключе вышедшего дольше, чем владелец не заходит. Окно снова временное, как и обещает ADR-018.
- Полоса «нужен новый ключ комнаты: подтвердите ключ @x» переживает перезагрузку: после `ready` владелец снова упирается в тот же неподтверждённый ключ и снова её показывает. Отдельного поля в `chats` для этого не нужно.
- Признак — метаданные комнаты, серверу и так известные: он знает состав и знает, что ключ не менялся. Нового про ключи сервер не узнаёт.
- Клиент по-прежнему решает сам, делать ли rekey: сервер только помнит, что состав уменьшился.
