diff --git a/TASKS/02-12.04.26-web-push-direct-message/01-ТЗ-web-push-direct-message.md b/TASKS/02-12.04.26-web-push-direct-message/01-ТЗ-web-push-direct-message.md new file mode 100644 index 0000000..7019aed --- /dev/null +++ b/TASKS/02-12.04.26-web-push-direct-message/01-ТЗ-web-push-direct-message.md @@ -0,0 +1,153 @@ +# Задача 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 по `deviceKey` отправителя. + +--- + +## Предварительная спецификация подписанного пакета (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`, поднимает `deviceKey` пользователя; +- проверяет подпись 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 делается по `deviceKey` отправителя через `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__`. + diff --git a/shine-UI/firebase-messaging-sw.js b/shine-UI/firebase-messaging-sw.js index 28ce1aa..cf68f0a 100644 --- a/shine-UI/firebase-messaging-sw.js +++ b/shine-UI/firebase-messaging-sw.js @@ -1,30 +1,20 @@ -/* global importScripts, firebase */ -importScripts('https://www.gstatic.com/firebasejs/10.12.2/firebase-app-compat.js'); -importScripts('https://www.gstatic.com/firebasejs/10.12.2/firebase-messaging-compat.js'); - self.addEventListener('install', () => self.skipWaiting()); self.addEventListener('activate', (event) => event.waitUntil(self.clients.claim())); -// Заполните теми же значениями, что и в shine-UI/index.html -const FIREBASE_CONFIG = { - apiKey: '', - authDomain: '', - projectId: '', - messagingSenderId: '', - appId: '', -}; - -if (FIREBASE_CONFIG.apiKey && firebase && firebase.messaging) { - if (!firebase.apps.length) { - firebase.initializeApp(FIREBASE_CONFIG); +self.addEventListener('push', (event) => { + let body = 'Новое сообщение SHiNE'; + try { + if (event.data) { + const text = event.data.text(); + body = text || body; + } + } catch { + // ignore } - const messaging = firebase.messaging(); - messaging.onBackgroundMessage((payload) => { - const title = payload?.notification?.title || 'Новое сообщение'; - const options = { - body: payload?.notification?.body || '', - data: payload?.data || {}, - }; - self.registration.showNotification(title, options); - }); -} + + event.waitUntil(self.registration.showNotification('SHiNE: входящее сообщение', { + body, + tag: 'shine-direct-message', + renotify: true, + })); +}); diff --git a/shine-UI/index.html b/shine-UI/index.html index 6272b58..e452966 100644 --- a/shine-UI/index.html +++ b/shine-UI/index.html @@ -27,17 +27,9 @@
- -