SHiNE-server/TASKS/02-12.04.26-web-push-direct-message/01-ТЗ-web-push-direct-message.md
2026-06-22 21:57:09 +04:00

154 lines
8.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Задача 02: Web Push + подписанный API отправки личных сообщений
## Контекст (по текущему состоянию проекта)
- Уже есть JSON WebSocket API для личных сообщений: `SendDirectMessage`, `AckIncomingMessage`, `UpsertPushToken`.
- Сейчас серверный fallback-пуш реализован через FCM (`FcmPushSender`) и ключ `fcm.server.key`.
- Клиент уже регистрирует service worker и токен Firebase, затем аплоадит push token на сервер.
## Цель
Добавить полностью рабочий сценарий доставки личных сообщений с приоритетом:
1) онлайн-доставка в активную WebSocket-сессию;
2) если не подтверждено — Web Push;
3) поддержать отдельный API отправки без авторизации, где доступ проверяется цифровой подписью Ed25519 по `clientKey` отправителя.
---
## Предварительная спецификация подписанного пакета (v1)
> ВАЖНО: финально фиксируется после уточнений по endian/кодировкам/лимитам.
Пакет (binary):
1. `prefix` — ASCII-константа, например `SHINE_MESSAGE`.
2. `toLoginLen` — 1 байт.
3. `toLogin` — ASCII, длина = `toLoginLen`.
4. `fromLoginLen` — 1 байт.
5. `fromLogin` — ASCII, длина = `fromLoginLen`.
6. `timeMs` — 8 байт (unix ms).
7. `nonce32` — 4 байта случайное число.
8. `messageType` — 4 байта.
9. `targetMode` — 1 байт:
- `0` = всем сессиям пользователя,
- `1` = конкретной сессии.
10. Если `targetMode=1`:
- `sessionIdLen` — 1 байт,
- `sessionId` — ASCII.
11. `messageLen` — 2 байта.
12. `messageBytes` — бинарные данные длиной `messageLen`.
13. `signature64` — 64 байта, Ed25519 подпись всего блока **без** `signature64`.
Ограничения (первичный draft):
- общий размер пакета ≤ 4000 байт;
- логины/префикс/идентификатор сессии — ASCII;
- повторы отсекаются по `(fromLogin, timeMs, nonce32)` в окне TTL.
---
## Сервер: что доработать
### 1) Новый endpoint без авторизации
Операция (через WS JSON обертку) условно `SendSignedDirectMessage`:
- принимает пакет (base64 binary blob);
- парсит и валидирует формат;
- достает `fromLogin`, поднимает `clientKey` пользователя;
- проверяет подпись Ed25519;
- проверяет анти-replay (time window + nonce);
- отправляет сообщение по правилам маршрутизации;
- пишет результат (messageId, каналы доставки, причины недоставки).
### 2) Маршрутизация доставки
Для `targetMode=1`:
- если целевая сессия онлайн и ACK пришел вовремя — успех;
- иначе отправка в Web Push этой сессии (если есть subscription).
Для `targetMode=0`:
- обход всех сессий пользователя;
- сначала online delivery + ACK;
- для непринятых/офлайн — Web Push по соответствующим subscription;
- если subscription отсутствует — тихий skip.
### 3) Миграция от FCM к Web Push
- добавить конфиг VAPID (`webpush.public.key`, `webpush.private.key`, `webpush.subject`);
- хранить на сервере не только token, а web-push subscription (endpoint + keys);
- сделать отправщик Web Push и заменить/расширить текущий `FcmPushSender`.
### 4) Безопасность
- строгая ASCII-валидация логинов/sessionId;
- лимиты длины всех полей;
- rate limit на endpoint;
- audit-лог неуспешных проверок подписи/формата;
- защита от replay.
---
## Клиент (shine-UI): что доработать
1. Перейти на стандартный Web Push flow:
- регистрация service worker;
- `PushManager.subscribe(...)` с VAPID public key;
- отправка subscription на сервер (`UpsertPushSubscription` или расширение `UpsertPushToken`).
2. Service worker:
- `push` handler получает payload целиком;
- показывает системное уведомление;
- при клике открывает/фокусирует нужный чат.
3. Online-сообщения:
- сохранить текущий event-канал `IncomingDirectMessage`;
- обязателен ACK (`AckIncomingMessage` уже есть).
4. Keep-alive:
- UI отправляет `Ping` раз в 60 секунд при активной сессии.
---
## Документация
Сделать отдельный документ настройки Web Push:
- как сгенерировать VAPID ключи;
- какие параметры прописать на сервере и в UI;
- как проверить локально e2e (онлайн + офлайн пуш);
- ограничения payload и рекомендации по ретраям.
---
## Этапы реализации (предложение)
1. Зафиксировать бинарный формат + валидации.
2. Реализовать серверный parser/validator/signature verify/replay guard.
3. Реализовать Web Push sender + storage subscription.
4. Подключить новый endpoint и маршрутизацию доставки.
5. Обновить UI (subscription + service worker + ping timer).
6. Добавить интеграционные тесты (online ACK / offline push / bad signature / replay / oversize).
7. Добавить документацию.
---
## Что нужно уточнить до разработки
1. Endian для `timeMs/nonce/messageType/messageLen` (big-endian или little-endian).
2. Что именно подписывается: строго весь префикс..messageBytes (без подписи) — подтвердить.
3. Диапазон допустимых `messageType`.
4. TTL окна для анти-replay (например 5 минут / 15 минут).
5. Лимиты длин для login/session/message.
6. Можно ли временно оставить FCM как fallback, пока не готов Web Push в проде.
7. Формат сообщения в `messageBytes`: opaque bytes или UTF-8 строка.
## Статус реализации (12.04.2026)
### Что уже внедрено в коде
- `SendDirectMessage` переведён на signed-binary payload (`blobB64`) без обязательной авторизации WS-сессии.
- Внедрён бинарный парсер пакета формата `SHiNE_msg + version(1) + ... + signature64`.
- Проверка подписи Ed25519 делается по `clientKey` отправителя через `shine-server-crypto` (`Ed25519Util`).
- Добавлен anti-replay guard `(from_login, time_ms, nonce)` с TTL 15 минут.
- Добавлено историческое хранилище `signed_direct_messages_history` с сырым пакетом `raw_packet`.
- Логика доставки: сначала WS+ACK, затем fallback на Web Push (по подписке конкретной session).
- Поле типа сообщения переведено на `uint16`, пока поддерживается только `1`.
- Для `targetMode=1` при несуществующей сессии возвращается `success` с `sessionNotFound=true` и `delivered=0`.
- UI переведён с Firebase/FCM на браузерный `PushManager.subscribe` + Service Worker `push`.
- Добавлен keep-alive ping из UI раз в 60 секунд при авторизованной сессии.
### Что настроить в окружении
- В `application.properties` задать:
- `webpush.vapid.public`
- `webpush.vapid.private`
- `webpush.vapid.subject`
- В `shine-UI/index.html` задать публичный VAPID ключ в `window.__SHINE_WEBPUSH_VAPID_PUBLIC_KEY__`.