Что сделано:\n- публичный API сценария входа через доверенное устройство переведён на TrustedDeviceLogin\n- добавлен GetTrustedDeviceLoginSettings\n- отсутствие записи настроек на сервере теперь трактуется как enabled=true и hasPassword=false\n- ttlSeconds убран из клиентского API, TTL заявки фиксирован на сервере: 300 секунд\n- в shine-UI добавлен отдельный экран настроек входа через устройство и статус на основном экране\n- browser wallet переведён на новые TrustedDeviceLogin операции\n- в wallet добавлен выбор rootKey/deviceKey для будущего запроса подписи\n- документация API обновлена\n\nЧто ещё не проверено вручную end-to-end:\n- полный сценарий UI/plugin после этого деплоя не прогонялся руками до конца\n- сам signaling подписи в wallet всё ещё не реализован
485 lines
14 KiB
Markdown
485 lines
14 KiB
Markdown
# API для разработчиков: Управление сессиями
|
||
|
||
Этот файл описывает методы, которые используются уже после успешной авторизации пользователя в сессию.
|
||
|
||
Здесь два метода:
|
||
|
||
- `ListSessions` — получить список активных сессий пользователя;
|
||
- `CloseActiveSession` — закрыть одну из активных сессий.
|
||
|
||
Дополнительно в этом же слое управления сессиями появился сценарий pairing через доверенную уже авторизованную сессию пользователя:
|
||
|
||
- `GetTrustedDeviceLoginSettings`
|
||
- `UpsertTrustedDeviceLoginSettings`
|
||
- `ListTrustedDeviceLoginRequests`
|
||
- `ApproveTrustedDeviceLogin`
|
||
- `RejectTrustedDeviceLogin`
|
||
- `CancelTrustedDeviceLogin`
|
||
|
||
Анонимное новое устройство работает с двумя связанными операциями:
|
||
|
||
- `StartTrustedDeviceLogin`
|
||
- `GetTrustedDeviceLoginStatus`
|
||
|
||
Логика раздела такая:
|
||
|
||
- сначала пользователь проходит `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. TrustedDeviceLogin через доверенную сессию
|
||
|
||
Этот блок относится к сценарию добавления новой сессии через доверенное устройство пользователя.
|
||
|
||
### 5.1. `GetTrustedDeviceLoginSettings`
|
||
|
||
Доступно для любой уже авторизованной доверенной сессии пользователя.
|
||
|
||
### Запрос
|
||
|
||
```json
|
||
{
|
||
"op": "GetTrustedDeviceLoginSettings",
|
||
"requestId": "trusted-login-get-001",
|
||
"payload": {
|
||
}
|
||
}
|
||
```
|
||
|
||
### Успешный ответ
|
||
|
||
```json
|
||
{
|
||
"op": "GetTrustedDeviceLoginSettings",
|
||
"requestId": "trusted-login-get-001",
|
||
"status": 200,
|
||
"ok": true,
|
||
"payload": {
|
||
"enabled": true,
|
||
"hasPassword": false
|
||
}
|
||
}
|
||
```
|
||
|
||
Если отдельной записи настроек на сервере ещё нет, сервер считает состояние по умолчанию таким:
|
||
|
||
- `enabled = true`
|
||
- `hasPassword = false`
|
||
|
||
### Ошибки
|
||
|
||
- `463 / PAIRING_REQUIRES_AUTH_SESSION` — операция вызвана без уже авторизованной доверенной сессии пользователя.
|
||
|
||
### 5.2. `UpsertTrustedDeviceLoginSettings`
|
||
|
||
Доступно для любой уже авторизованной доверенной сессии пользователя.
|
||
|
||
### Запрос
|
||
|
||
```json
|
||
{
|
||
"op": "UpsertTrustedDeviceLoginSettings",
|
||
"requestId": "esp-set-001",
|
||
"payload": {
|
||
"enabled": true,
|
||
"passwordHash": "sha256$0123abcd..."
|
||
}
|
||
}
|
||
```
|
||
|
||
Если вход через доверенное устройство должен работать **без доп. пароля**, клиент включает его с пустым `passwordHash`.
|
||
|
||
Если `enabled = false`, сервер автоматически удаляет пароль и запрещает вход через другое устройство.
|
||
|
||
Формат непустого `passwordHash`:
|
||
|
||
```text
|
||
sha256$<hex( SHA-256("shine-pairing|" + lower(login.trim()) + "|" + password) )>
|
||
```
|
||
|
||
### Успешный ответ
|
||
|
||
```json
|
||
{
|
||
"op": "UpsertTrustedDeviceLoginSettings",
|
||
"requestId": "esp-set-001",
|
||
"status": 200,
|
||
"ok": true,
|
||
"payload": {
|
||
"enabled": true,
|
||
"hasPassword": true
|
||
}
|
||
}
|
||
```
|
||
|
||
### Ошибки
|
||
|
||
- `463 / PAIRING_REQUIRES_AUTH_SESSION` — операция вызвана без уже авторизованной доверенной сессии пользователя.
|
||
|
||
### 5.3. `StartTrustedDeviceLogin`
|
||
|
||
Эта операция доступна без уже существующей пользовательской сессии.
|
||
|
||
### Запрос
|
||
|
||
```json
|
||
{
|
||
"op": "StartTrustedDeviceLogin",
|
||
"requestId": "esp-start-001",
|
||
"payload": {
|
||
"login": "alice",
|
||
"passwordHash": "sha256$0123abcd...",
|
||
"requesterSessionKey": "ed25519/BASE64_PUBLIC_KEY",
|
||
"requesterSessionType": 1,
|
||
"requesterClientPlatform": "Android",
|
||
"payloadType": 1
|
||
}
|
||
}
|
||
```
|
||
|
||
Если на доверённом устройстве вход включён **без доп. пароля**, новое устройство может отправить пустой `passwordHash`.
|
||
|
||
Поле `trustedSessionOnline` показывает, что у пользователя сейчас есть хотя бы одна онлайн доверенная сессия, способная принять pairing-заявку.
|
||
|
||
TTL заявки фиксирован на сервере и сейчас всегда равен `300` секундам.
|
||
|
||
### Успешный ответ
|
||
|
||
```json
|
||
{
|
||
"op": "StartTrustedDeviceLogin",
|
||
"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` — pairing-пароль не подходит. Та же ошибка возвращается и если новое устройство ввело пароль, а у пользователя режим pairing включён без пароля.
|
||
- `422 / PAIRING_NO_TRUSTED_SESSION_ONLINE` — сейчас нет ни одной онлайн доверённой сессии пользователя, поэтому код не создаётся.
|
||
- `429 / PAIRING_RATE_LIMITED`
|
||
|
||
### 5.4. `ListTrustedDeviceLoginRequests`
|
||
|
||
Доступно для любой уже авторизованной доверенной сессии пользователя.
|
||
Возвращает только реально активные pending-заявки со `state = created`. Уже `approved` и `rejected` заявки в этот список больше не попадают.
|
||
|
||
### Успешный ответ
|
||
|
||
```json
|
||
{
|
||
"op": "ListTrustedDeviceLoginRequests",
|
||
"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.5. `ApproveTrustedDeviceLogin`
|
||
|
||
Доступно для любой уже авторизованной доверенной сессии пользователя.
|
||
|
||
### Запрос
|
||
|
||
```json
|
||
{
|
||
"op": "ApproveTrustedDeviceLogin",
|
||
"requestId": "esp-approve-001",
|
||
"payload": {
|
||
"pairingId": "base64url",
|
||
"encryptedPayload": "BASE64_OR_OTHER_OPAQUE_PAYLOAD"
|
||
}
|
||
}
|
||
```
|
||
|
||
### Успешный ответ
|
||
|
||
```json
|
||
{
|
||
"op": "ApproveTrustedDeviceLogin",
|
||
"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.6. `RejectTrustedDeviceLogin`
|
||
|
||
Доступно для любой уже авторизованной доверенной сессии пользователя. Похоже на approve, но переводит заявку в `state=rejected`.
|
||
|
||
### 5.7. `GetTrustedDeviceLoginStatus`
|
||
|
||
Операция для нового устройства.
|
||
|
||
### Запрос
|
||
|
||
```json
|
||
{
|
||
"op": "GetTrustedDeviceLoginStatus",
|
||
"requestId": "esp-status-001",
|
||
"payload": {
|
||
"pairingId": "base64url"
|
||
}
|
||
}
|
||
```
|
||
|
||
### Успешный ответ после approve
|
||
|
||
```json
|
||
{
|
||
"op": "GetTrustedDeviceLoginStatus",
|
||
"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`
|
||
- `canceled`
|
||
- `expired`
|
||
|
||
### 5.8. `CancelTrustedDeviceLogin`
|
||
|
||
Операция для нового устройства, которое уже создало pairing-заявку и хочет принудительно снять ожидание до истечения TTL.
|
||
|
||
### Запрос
|
||
|
||
```json
|
||
{
|
||
"op": "CancelTrustedDeviceLogin",
|
||
"requestId": "esp-cancel-001",
|
||
"payload": {
|
||
"pairingId": "base64url",
|
||
"requesterSessionKey": "ed25519/BASE64_PUBLIC_KEY"
|
||
}
|
||
}
|
||
```
|
||
|
||
### Успешный ответ
|
||
|
||
```json
|
||
{
|
||
"op": "CancelTrustedDeviceLogin",
|
||
"requestId": "esp-cancel-001",
|
||
"status": 200,
|
||
"ok": true,
|
||
"payload": {
|
||
"pairingId": "base64url",
|
||
"state": "canceled"
|
||
}
|
||
}
|
||
```
|
||
|
||
### Ошибки
|
||
|
||
- `400 / EMPTY_PAIRING_ID`
|
||
- `400 / EMPTY_REQUESTER_SESSION_KEY`
|
||
- `400 / BAD_REQUESTER_SESSION_KEY`
|
||
- `404 / PAIRING_NOT_FOUND`
|
||
- `422 / PAIRING_OF_ANOTHER_REQUESTER`
|
||
- `422 / PAIRING_NOT_PENDING`
|