# API для разработчиков: Управление сессиями Этот файл описывает методы, которые используются уже после успешной авторизации пользователя в сессию. Здесь два метода: - `ListSessions` — получить список активных сессий пользователя; - `CloseActiveSession` — закрыть одну из активных сессий. Дополнительно в этом же слое управления сессиями появился сценарий pairing через доверенную уже авторизованную сессию пользователя: - `UpsertEspPairingSettings` - `ListEspPairingRequests` - `ApproveEspPairing` - `RejectEspPairing` Анонимное новое устройство работает с двумя связанными операциями: - `StartEspPairing` - `GetEspPairingStatus` Логика раздела такая: - сначала пользователь проходит `SessionLogin`; - после этого сервер считает соединение авторизованным; - уже в этом состоянии клиент может читать список сессий и управлять ими. То есть это не этап создания или входа в сессию, а этап последующего контроля уже существующих активных сессий. ## 1. `ListSessions` Доступно только после успешного `SessionLogin`. ### Запрос ```json { "op": "ListSessions", "requestId": "list-001", "payload": { } } ``` ### Успешный ответ ```json { "op": "ListSessions", "requestId": "list-001", "status": 200, "ok": true, "payload": { "sessions": [ { "sessionId": "sess_7c5e5c4b", "sessionType": 1, "clientPlatform": "Web", "onlineOnThisServer": true, "clientInfoFromClient": "Android 15; Pixel 9", "clientInfoFromRequest": "UA=Java-http-client/17.0.18; remote=127.0.0.1", "geo": "RU/Moscow", "lastAuthenticatedAtMs": 1774600010500 } ] } } ``` ### Специфические коды ошибок `ListSessions` - `422 / NOT_AUTHENTICATED` — запрос доступен только после успешного `SessionLogin`. - `501 / DB_ERROR_LIST_SESSIONS` — ошибка БД при чтении списка активных сессий. - `500 / INTERNAL_ERROR` — непредвиденная внутренняя ошибка сервера. ### Поля одной сессии в `ListSessions` - `sessionId` — идентификатор активной сессии; - `sessionType` — числовой код типа сессии: - `1` — клиент; - `50` — кошелёк; - `100` — homeserver; - `clientPlatform` — строка платформы, как её прислал клиент; - `onlineOnThisServer` — `true`, если эта сессия сейчас держит живое WebSocket-подключение именно к данному серверу; - `clientInfoFromClient` — краткая строка клиента; - `clientInfoFromRequest` — строка, собранная сервером из запроса; - `geo` — страна/город или fallback-строка; - `lastAuthenticatedAtMs` — время последней успешной авторизации этой сессии. --- ## 2. `CloseActiveSession` Доступно только после успешного `SessionLogin`. ### Запрос ```json { "op": "CloseActiveSession", "requestId": "close-001", "payload": { "sessionId": "sess_7c5e5c4b" } } ``` ### Успешный ответ ```json { "op": "CloseActiveSession", "requestId": "close-001", "status": 200, "ok": true, "payload": { } } ``` ### Специфические коды ошибок `CloseActiveSession` - `422 / NOT_AUTHENTICATED` — запрос доступен только после успешного `SessionLogin`. - `400 / NO_SESSION_TO_CLOSE` — сервер не смог определить, какую сессию нужно закрыть. - `501 / DB_ERROR` — ошибка БД при поиске сессии или её удалении. - `422 / SESSION_NOT_FOUND` — целевая сессия не найдена. - `422 / SESSION_OF_ANOTHER_USER` — нельзя закрывать сессию другого пользователя. - `500 / INTERNAL_ERROR` — непредвиденная внутренняя ошибка сервера. --- ## 3. Пример ошибки ```json { "op": "CloseActiveSession", "requestId": "close-001", "status": 403, "ok": false, "error": "NOT_AUTHENTICATED", "message": "Операция доступна только для авторизованных пользователей", "payload": { } } ``` ## 4. Формат `sessionId` Текущее серверное значение `sessionId` генерируется как: - случайные **32 байта** (`SecureRandom`), - кодирование в **стандартный Base64 RFC 4648** (алфавит `A-Z a-z 0-9 + /`), - **без padding** `=`. Практически это строка длиной около **43 символов** (для 32 байт без `=`). Пример реального формата: ``` K9v3nQ4u8jYk0a2p7cD4mLx1zR0sT5wV6bN8eH3fQ1M ``` Важно: это **не человеко-читаемое имя**, а непрозрачный идентификатор. Нужно передавать его как есть, без нормализации регистра и без URL-экранирования внутри JSON. --- ## 5. ESP pairing через доверенную сессию Этот блок относится к сценарию добавления новой сессии через доверенное устройство пользователя. ### 5.1. `UpsertEspPairingSettings` Доступно для любой уже авторизованной доверенной сессии пользователя. ### Запрос ```json { "op": "UpsertEspPairingSettings", "requestId": "esp-set-001", "payload": { "enabled": true, "passwordHash": "argon2id$...", "ttlSeconds": 180 } } ``` Если pairing должен работать **без доп. пароля**, клиент может включить его с пустым `passwordHash`. ### Успешный ответ ```json { "op": "UpsertEspPairingSettings", "requestId": "esp-set-001", "status": 200, "ok": true, "payload": { "enabled": true, "ttlSeconds": 180 } } ``` ### Ошибки - `463 / PAIRING_REQUIRES_AUTH_SESSION` — операция вызвана без уже авторизованной доверенной сессии пользователя. ### 5.2. `StartEspPairing` Эта операция доступна без уже существующей пользовательской сессии. ### Запрос ```json { "op": "StartEspPairing", "requestId": "esp-start-001", "payload": { "login": "alice", "passwordHash": "argon2id$...", "requesterSessionKey": "ed25519/BASE64_PUBLIC_KEY", "requesterSessionType": 1, "requesterClientPlatform": "Android", "payloadType": 1 } } ``` Если на доверённом устройстве pairing включён **без доп. пароля**, новое устройство может отправить пустой `passwordHash`. Поле `trustedSessionOnline` показывает, что у пользователя сейчас есть хотя бы одна онлайн доверенная сессия, способная принять pairing-заявку. ### Успешный ответ ```json { "op": "StartEspPairing", "requestId": "esp-start-001", "status": 200, "ok": true, "payload": { "pairingId": "base64url", "state": "created", "shortCode": "4920709", "fingerprintB58": "ASvYDPQidnAroKzQjtCjTuEQE8ckktV5nmmhYRhDzGaA", "expiresAtMs": 1781441990538, "trustedSessionOnline": true } } ``` ### Ошибки - `400 / EMPTY_LOGIN` - `400 / EMPTY_REQUESTER_SESSION_KEY` - `400 / BAD_REQUESTER_SESSION_KEY` - `400 / BAD_SESSION_TYPE` - `400 / BAD_PAYLOAD_TYPE` - `422 / PAIRING_NOT_AVAILABLE` - `422 / PAIRING_PASSWORD_INVALID` - `429 / PAIRING_RATE_LIMITED` ### 5.3. `ListEspPairingRequests` Доступно для любой уже авторизованной доверенной сессии пользователя. ### Успешный ответ ```json { "op": "ListEspPairingRequests", "requestId": "esp-list-001", "status": 200, "ok": true, "payload": { "requests": [ { "pairingId": "base64url", "state": "created", "requesterSessionKey": "ed25519/BASE64_PUBLIC_KEY", "requesterSessionType": 1, "requesterClientPlatform": "Android", "payloadType": 1, "shortCode": "4920709", "fingerprintB58": "ASvYDPQidnAroKzQjtCjTuEQE8ckktV5nmmhYRhDzGaA", "createdAtMs": 1781441810538, "expiresAtMs": 1781441990538, "deliveredToHomeserver": true } ] } } ``` ### Ошибки - `463 / PAIRING_REQUIRES_AUTH_SESSION` ### 5.4. `ApproveEspPairing` Доступно для любой уже авторизованной доверенной сессии пользователя. ### Запрос ```json { "op": "ApproveEspPairing", "requestId": "esp-approve-001", "payload": { "pairingId": "base64url", "encryptedPayload": "BASE64_OR_OTHER_OPAQUE_PAYLOAD" } } ``` ### Успешный ответ ```json { "op": "ApproveEspPairing", "requestId": "esp-approve-001", "status": 200, "ok": true, "payload": { "pairingId": "base64url", "state": "approved" } } ``` ### Ошибки - `400 / EMPTY_PAIRING_ID` - `400 / EMPTY_ENCRYPTED_PAYLOAD` - `404 / PAIRING_NOT_FOUND` - `422 / PAIRING_OF_ANOTHER_USER` - `422 / PAIRING_NOT_PENDING` - `422 / PAIRING_EXPIRED` - `463 / PAIRING_REQUIRES_AUTH_SESSION` ### 5.5. `RejectEspPairing` Доступно для любой уже авторизованной доверенной сессии пользователя. Похоже на approve, но переводит заявку в `state=rejected`. ### 5.6. `GetEspPairingStatus` Операция для нового устройства. ### Запрос ```json { "op": "GetEspPairingStatus", "requestId": "esp-status-001", "payload": { "pairingId": "base64url" } } ``` ### Успешный ответ после approve ```json { "op": "GetEspPairingStatus", "requestId": "esp-status-001", "status": 200, "ok": true, "payload": { "pairingId": "base64url", "state": "approved", "shortCode": "4920709", "fingerprintB58": "ASvYDPQidnAroKzQjtCjTuEQE8ckktV5nmmhYRhDzGaA", "payloadType": 1, "encryptedPayload": "AQIDBA==", "expiresAtMs": 1781441990538 } } ``` ### Возможные `state` - `created` - `approved` - `rejected` - `expired`