# 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
  }
}
```

### Успешный ответ

```json
{
  "op": "UpsertEspPairingSettings",
  "requestId": "esp-set-001",
  "status": 200,
  "ok": true,
  "payload": {
    "enabled": true,
    "ttlSeconds": 180
  }
}
```

### Ошибки

- `400 / EMPTY_PASSWORD_HASH` — попытка включить pairing без `passwordHash`.
- `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
  }
}
```

Поле `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_PASSWORD_HASH`
- `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`
