commit 3d814112a9634f56c6f0d07e17dc1492bc840c29 Author: Claude Date: Thu Aug 20 09:17:37 2026 +0000 Заложить репозиторий: философия, архитектура, модель угроз, ADR Документы фиксируют принятые проектные решения Bare: клиент без фреймворков и сборки, Go-сервер одним бинарём, E2EE на WebCrypto, сервер-реле без истории, локальная история с экспортом/импортом, Web Push + VAPID. Кода нет — сначала документы. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01SW4ZXtKn5NjX8NH3Lhm2ND diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..da68a24 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +# бинарь +/bare + +# локальная база +*.db +*.db-* diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..b6abd98 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,19 @@ +# Правила работы с репозиторием Bare + +Перед любой работой прочитай `docs/philosophy.md` и `docs/architecture.md`. Всё в них — принятые решения, а не предложения. + +## Жёсткие ограничения + +- Клиент: HTML, CSS, vanilla JS, нативные браузерные API. Никаких фреймворков, npm-зависимостей и сборки — ES-модули как есть. +- Сервер: Go, стандартная библиотека. Допущены ровно три внешних пакета: webpush-go, драйвер SQLite, argon2. Ничего сверх — без обсуждения. +- Криптография на клиенте: только WebCrypto. + +## Процесс + +- Новые фичи и зависимости по умолчанию отклоняются. Порядок: обсуждение → ADR → код. +- Любое изменение архитектуры — через новый ADR в `docs/decisions/` (формат: контекст / решение / следствия). +- Простота важнее функций. Сомневаешься — не добавляй. + +## Документация + +Язык — русский. Стиль — сжатый и декларативный: короткие утвердительные абзацы, без маркетинговой воды и канцелярита. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..9535ee8 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Bare contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..08deb9f --- /dev/null +++ b/README.md @@ -0,0 +1,23 @@ +# Bare + +Максимально простой веб-чат. Web-native PWA на ванильных технологиях: HTML, CSS, vanilla JS — и Go-сервер одним бинарём. Ноль npm-зависимостей, никакой сборки. + +Bare — маленький независимый инструмент, а не конкурент Discord, Slack или Telegram. Название буквальное: голый, без лишнего, ничего не спрятано за слоями абстракций. + +Сервер — реле, а не архив: он передаёт зашифрованные сообщения и забывает их. История живёт только на устройстве. E2EE по умолчанию — оператор видит только шифротекст. Забыл пароль — значит забыл: восстановления нет, потому что пароль — материал ключа. + +## Документация + +- [Философия](docs/philosophy.md) — манифест +- [Архитектура](docs/architecture.md) — обзор системы +- [Модель угроз](docs/threat-model.md) — от чего защищаемся и от чего нет +- [Решения](docs/decisions/) — ADR по ключевым решениям +- [Открытые вопросы](docs/open-questions.md) + +## Статус + +Проектирование. Кода ещё нет — сначала документы. + +## Лицензия + +[MIT](LICENSE) diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..9c311b4 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,55 @@ +# Архитектура + +Bare — это PWA-клиент на ванильных веб-технологиях и Go-сервер одним бинарём. Между ними — голый HTTPS: приём сообщений через SSE, отправка обычным `fetch POST`. Чат поверх голого HTTP. + +## Стек + +Клиент: HTML + CSS + vanilla JS, ES-модули без сборки, один service worker, manifest.json. PWA. Ноль npm-зависимостей. + +Сервер: Go, стандартная библиотека плюс ровно три внешних пакета — webpush-go, драйвер SQLite, argon2. База — SQLite. HTTPS обязателен: без него не работают service worker и пуши. + +## Аккаунты + +Регистрация — ник и пароль. Ник уникален и является идентификатором пользователя. Без email, телефона, OAuth и интеграций. Восстановления пароля нет. + +Серверная аутентификация: Argon2id, сессия в httpOnly cookie. + +## E2EE + +Вся клиентская криптография — WebCrypto, без крипто-библиотек. + +Идентичность пользователя — ECDH-пара (P-256). Приватный ключ шифруется ключом, выведенным из пароля (PBKDF2), и хранится на сервере как блоб — сервер видит только шифротекст. Отсюда два следствия. Сброс пароля невозможен by design. Мультидевайс-вход прост: новый девайс вводит пароль, скачивает блоб, расшифровывает ключ. + +Чаты 1:1: ECDH shared secret → AES-GCM. + +Комнаты: у комнаты симметричный ключ, он раздаётся участникам зашифрованным на их публичные ключи. При изменении состава — rekey. Новый участник не видит сообщений до своего вступления — их и не существует нигде, кроме устройств участников. + +Forward secrecy — осознанный non-goal v1. + +## Хранение + +Сервер хранит только три вещи: аккаунты (ник, argon2-хеш, зашифрованный ключевой блоб), метаданные комнат и контактов, транзитную очередь зашифрованных недоставленных сообщений. Очередь per-device: доставлено и подтверждено ACK — удалено с сервера; не забрано за 30 дней — удалено. + +Клиент хранит историю в IndexedDB. messageId — ULID/UUIDv7: хронологическая сортировка и идемпотентный merge. Составной индекс (chatId, messageId). Пагинация курсором по ~50 сообщений, виртуализация списка в DOM. + +При старте клиент запрашивает `navigator.storage.persist()` и показывает занятое место через `storage.estimate()`. + +## Экспорт и импорт истории + +Экспорт: вся локальная история сериализуется, шифруется отдельной парольной фразой (запрашивается в момент экспорта) и сохраняется одним файлом `.bare`. Импорт: файл плюс фраза, merge в IndexedDB по messageId, идемпотентно — повторный импорт и склейка истории с двух устройств не создают дублей. + +Это единственный механизм переноса истории между устройствами. Осознанно. + +## Пуши + +Web Push + VAPID. Одна пара ключей, никаких регистраций и оплат у вендоров, никакого Firebase SDK. + +Пуш — сигнал, не транспорт: содержимое всегда догоняется через очередь при открытии. Текст пуша generic («имя: новое сообщение») — сервер не знает плейнтекста. Declarative Web Push не используем: несовместим с E2EE. + +iOS: пуши работают только у PWA, установленного на экран «Домой», поэтому онбординг-баннер установки — обязательная часть продукта. Разрешение на уведомления запрашивается после осмысленного действия (первое отправленное сообщение), не при входе. + +Сервер обрабатывает 404/410 от push-сервисов и чистит мёртвые подписки. + +## Scope v1 + +Чаты 1:1 и комнаты. Только текст и эмодзи (эмодзи — юникод, отдельной фичи нет). Экспорт/импорт истории. Пуши на всех платформах. Всё остальное — за пределами v1. diff --git a/docs/decisions/001-vanilla-client.md b/docs/decisions/001-vanilla-client.md new file mode 100644 index 0000000..1d5c201 --- /dev/null +++ b/docs/decisions/001-vanilla-client.md @@ -0,0 +1,16 @@ +# ADR-001: Клиент на ванильных веб-технологиях, без сборки + +## Контекст + +Bare обещает аудируемость: любой должен иметь возможность проверить глазами, что делает код, который исполняет его браузер. Фреймворки, npm-зависимости и сборка превращают исходники в артефакт, который никто не читает. + +## Решение + +Клиент — HTML, CSS и vanilla JS на нативных браузерных API. ES-модули отдаются как есть, без транспиляции и бандлинга. Ноль npm-зависимостей. Один service worker и manifest.json — полноценный PWA. + +## Следствия + +- Код в продакшене байт в байт совпадает с кодом в репозитории. +- Нет supply-chain-рисков npm и нет тулчейна, который надо поддерживать. +- Состояние, рендеринг и роутинг пишем руками — отказ от удобств фреймворков осознанный. +- Поддерживаем только современные браузеры с ES-модулями и WebCrypto. diff --git a/docs/decisions/002-go-server.md b/docs/decisions/002-go-server.md new file mode 100644 index 0000000..0e2436a --- /dev/null +++ b/docs/decisions/002-go-server.md @@ -0,0 +1,15 @@ +# ADR-002: Сервер на Go, один бинарь + +## Контекст + +Серверу Bare нужно немного: HTTP, SSE, SQLite, хеширование паролей, Web Push. Простота развёртывания и аудита важнее богатства экосистемы. + +## Решение + +Сервер пишется на Go со стандартной библиотекой. Допущены ровно три внешних пакета: webpush-go, драйвер SQLite, argon2. Результат сборки — один бинарь. + +## Следствия + +- Развёртывание: скопировать бинарь, запустить. Без рантаймов и обязательных контейнеров. +- Любая новая зависимость требует обсуждения и нового ADR. +- Часть вещей (роутинг, миграции) пишем руками поверх stdlib. diff --git a/docs/decisions/003-sqlite.md b/docs/decisions/003-sqlite.md new file mode 100644 index 0000000..55a7485 --- /dev/null +++ b/docs/decisions/003-sqlite.md @@ -0,0 +1,15 @@ +# ADR-003: SQLite как единственная база + +## Контекст + +Сервер хранит мало: аккаунты, метаданные комнат и контактов, транзитную очередь. Отдельный сервер БД — лишняя движущаяся часть, противоречащая идее одного бинаря. + +## Решение + +Вся серверная persistence — SQLite, один файл рядом с бинарём. + +## Следствия + +- Бэкап — копия одного файла. +- Нет сетевой БД — нет её настройки, аутентификации и отказов. +- Вертикальный потолок масштабирования принимаем осознанно: Bare — маленький инструмент, не платформа. diff --git a/docs/decisions/004-sse-and-post.md b/docs/decisions/004-sse-and-post.md new file mode 100644 index 0000000..2374de5 --- /dev/null +++ b/docs/decisions/004-sse-and-post.md @@ -0,0 +1,15 @@ +# ADR-004: Realtime через SSE и fetch POST + +## Контекст + +Чату нужна доставка сообщений в реальном времени. WebSocket — отдельный протокол со своим жизненным циклом, капризами прокси и кастомным фреймингом. Хочется остаться в рамках голого HTTP. + +## Решение + +Приём сообщений — Server-Sent Events. Отправка — обычный `fetch POST`. Чат поверх голого HTTP. + +## Следствия + +- Никакого кастомного протокола: всё видно в DevTools как обычные HTTP-запросы, реконнект SSE встроен в браузер. +- Работает везде, где работает HTTPS, без апгрейда соединения. +- Двунаправленного канала нет — он и не нужен: у отправки и приёма разные пути. diff --git a/docs/decisions/005-accounts.md b/docs/decisions/005-accounts.md new file mode 100644 index 0000000..a6d2767 --- /dev/null +++ b/docs/decisions/005-accounts.md @@ -0,0 +1,16 @@ +# ADR-005: Аккаунт — ник и пароль + +## Контекст + +Email, телефон и OAuth тянут за собой внешние сервисы, интеграции и утечку идентичности. Bare — независимый инструмент без внешних завязок. + +## Решение + +Регистрация — ник и пароль. Ник уникален и является идентификатором пользователя. Без email, телефона, OAuth и восстановления пароля. Серверная аутентификация: Argon2id, сессия в httpOnly cookie. + +## Следствия + +- Ноль внешних сервисов в цикле регистрации и входа. +- Сервер не знает о пользователе ничего, кроме ника. +- Восстановления доступа нет — пароль ещё и материал ключа (ADR-006). +- Открытость регистрации (инвайты или нет) — открытый вопрос. diff --git a/docs/decisions/006-e2ee-webcrypto.md b/docs/decisions/006-e2ee-webcrypto.md new file mode 100644 index 0000000..3e76daa --- /dev/null +++ b/docs/decisions/006-e2ee-webcrypto.md @@ -0,0 +1,16 @@ +# ADR-006: E2EE на WebCrypto, ключ за паролем + +## Контекст + +Оператор не должен уметь читать сообщения. Крипто-библиотеки на клиенте противоречат нулю зависимостей и аудируемости — вся криптография должна быть нативной. + +## Решение + +Только WebCrypto. Идентичность пользователя — ECDH-пара P-256. Приватный ключ шифруется ключом, выведенным из пароля (PBKDF2 — единственный KDF в WebCrypto), и хранится на сервере как блоб. Чаты 1:1: ECDH shared secret → AES-GCM. + +## Следствия + +- Сервер видит только шифротекст — и блоба, и сообщений. +- Сброс пароля невозможен by design: пароль — материал ключа. Забыл — значит забыл. +- Мультидевайс-вход: новый девайс вводит пароль, скачивает блоб, расшифровывает ключ. +- Стойкость блоба к оффлайн-перебору равна стойкости пароля. diff --git a/docs/decisions/007-room-keys.md b/docs/decisions/007-room-keys.md new file mode 100644 index 0000000..fc14b81 --- /dev/null +++ b/docs/decisions/007-room-keys.md @@ -0,0 +1,15 @@ +# ADR-007: Симметричный ключ комнаты и rekey + +## Контекст + +Сообщение в комнате должны читать все участники, но не сервер. Шифровать каждое сообщение отдельно каждому участнику — квадратичный объём работы и трафика. + +## Решение + +У комнаты один симметричный ключ. Он раздаётся участникам зашифрованным на их публичные ключи. При любом изменении состава — rekey: новый ключ, новая раздача. + +## Следствия + +- Новый участник не видит сообщений до своего вступления — их и не существует нигде, кроме устройств участников. +- Вышедший участник не читает сообщения после rekey. +- Rekey выполняют клиенты; серверу ключи недоступны. diff --git a/docs/decisions/008-server-relay.md b/docs/decisions/008-server-relay.md new file mode 100644 index 0000000..ee28355 --- /dev/null +++ b/docs/decisions/008-server-relay.md @@ -0,0 +1,15 @@ +# ADR-008: Сервер — реле с per-device очередью + +## Контекст + +Сервер никогда не является местом, где живёт история (философия, п. 2). Но получатель бывает офлайн — сообщение надо где-то подержать до доставки. + +## Решение + +Сервер хранит транзитную очередь зашифрованных недоставленных сообщений, per-device. Доставлено и подтверждено ACK — удалено с сервера. Не забрано за 30 дней — удалено. Кроме очереди сервер хранит только аккаунты и метаданные комнат и контактов. + +## Следствия + +- На сервере нет истории — компрометация сервера не раскрывает переписку. +- Устройство, молчавшее больше 30 дней, теряет недоставленные сообщения. Осознанно. +- Нужна идентификация устройства для очередей — открытый вопрос. diff --git a/docs/decisions/009-local-history.md b/docs/decisions/009-local-history.md new file mode 100644 index 0000000..56cddea --- /dev/null +++ b/docs/decisions/009-local-history.md @@ -0,0 +1,15 @@ +# ADR-009: История — только на устройстве, в IndexedDB + +## Контекст + +История, живущая на сервере, делает сервер архивом и целью атак. У Bare история — собственность устройства. + +## Решение + +Клиент хранит историю в IndexedDB. messageId — ULID/UUIDv7: хронологическая сортировка и идемпотентный merge. Составной индекс (chatId, messageId). Пагинация курсором по ~50 сообщений, виртуализация списка в DOM. При старте — `navigator.storage.persist()`; занятое место показывается через `storage.estimate()`. + +## Следствия + +- Зашёл с другого устройства — там только новые сообщения. +- Потеря устройства без экспорта — потеря истории. +- Браузер может вычистить storage; `persist()` снижает риск, но не исключает его. diff --git a/docs/decisions/010-export-import.md b/docs/decisions/010-export-import.md new file mode 100644 index 0000000..284bc78 --- /dev/null +++ b/docs/decisions/010-export-import.md @@ -0,0 +1,15 @@ +# ADR-010: Перенос истории — только ручной экспорт/импорт + +## Контекст + +История живёт на устройстве (ADR-009), но людям нужны перенос на новое устройство и склейка истории с нескольких. Облачная синхронизация сделала бы сервер архивом — non-goal. + +## Решение + +Экспорт: вся локальная история сериализуется, шифруется отдельной парольной фразой (запрашивается в момент экспорта) и сохраняется одним файлом `.bare`. Импорт: файл плюс фраза, идемпотентный merge в IndexedDB по messageId. Это единственный механизм переноса истории между устройствами. + +## Следствия + +- Повторный импорт и склейка истории с двух устройств не создают дублей. +- Файл `.bare` самодостаточен и не зависит от пароля аккаунта. +- Перенос — явное действие пользователя. Автоматики не будет. Осознанно. diff --git a/docs/decisions/011-web-push.md b/docs/decisions/011-web-push.md new file mode 100644 index 0000000..967c297 --- /dev/null +++ b/docs/decisions/011-web-push.md @@ -0,0 +1,15 @@ +# ADR-011: Web Push + VAPID, пуш — сигнал + +## Контекст + +Без уведомлений чат бесполезен. Firebase SDK и вендорские кабинеты — зависимость и завязка, несовместимые с философией. + +## Решение + +Web Push + VAPID: одна пара ключей, никаких регистраций и оплат у вендоров, никакого Firebase SDK. Пуш — сигнал, не транспорт: содержимое догоняется через очередь при открытии. Текст пуша generic («имя: новое сообщение»). Declarative Web Push не используем — несовместим с E2EE. Сервер обрабатывает 404/410 от push-сервисов и чистит мёртвые подписки. + +## Следствия + +- Сервер не знает плейнтекста — и в пуше его нет. +- Доставка идёт через инфраструктуру вендоров браузеров (FCM/APNs/Mozilla) — свойство стандарта, не наша зависимость. +- iOS: пуши только у PWA на экране «Домой», поэтому онбординг-баннер установки — обязательная часть продукта. Разрешение запрашиваем после первого отправленного сообщения, не при входе. diff --git a/docs/decisions/012-no-forward-secrecy.md b/docs/decisions/012-no-forward-secrecy.md new file mode 100644 index 0000000..f792a2a --- /dev/null +++ b/docs/decisions/012-no-forward-secrecy.md @@ -0,0 +1,15 @@ +# ADR-012: Forward secrecy — non-goal v1 + +## Контекст + +FS требует ratcheting в духе Signal Protocol: состояние на каждую пару устройств, синхронизация, сложность, несовместимая с клиентом из нескольких читаемых файлов. + +## Решение + +В v1 forward secrecy нет. 1:1 — статический ECDH shared secret, комнаты — симметричный ключ до rekey. + +## Следствия + +- Компрометация приватного ключа раскрывает ранее записанные шифротексты. +- Модель угроз говорит об этом прямо (`docs/threat-model.md`). +- Возврат к FS позже возможен — отдельным ADR. diff --git a/docs/open-questions.md b/docs/open-questions.md new file mode 100644 index 0000000..558f8fd --- /dev/null +++ b/docs/open-questions.md @@ -0,0 +1,11 @@ +# Открытые вопросы + +Решения по этим пунктам ещё не приняты. Каждое принятое решение уходит в ADR и вычёркивается отсюда. + +- Регистрация: открытая или по инвайтам? +- Механика добавления контакта и приглашения в комнату: по нику? по ссылке? +- Лимиты: длина сообщения, rate limiting, антиспам. +- Смена пароля (= перешифровка ключевого блоба): в v1 или позже? +- Идентификация устройства для per-device очередей. +- Язык интерфейса (ru/en); нужна ли i18n. +- Визуальная айдентика: отдельный бриф будет добавлен в `docs/identity/`. diff --git a/docs/philosophy.md b/docs/philosophy.md new file mode 100644 index 0000000..e14ad72 --- /dev/null +++ b/docs/philosophy.md @@ -0,0 +1,44 @@ +# Философия + +Bare — максимально простой веб-чат. Utility, а не платформа. Название буквальное: голый, без лишнего, ничего не спрятано за слоями абстракций. + +## Принципы + +### 1. Bare = голый web + +HTML, CSS, vanilla JS, нативные браузерные API. Ноль npm-зависимостей на клиенте. Никакой сборки — ES-модули как есть. + +### 2. Сервер — реле, а не архив + +Сервер никогда не является местом, где живёт история. Он передаёт и забывает. + +### 3. История живёт только на устройстве + +Зашёл с другого места — там только новые сообщения. Перенос истории — явный ручной экспорт/импорт. + +### 4. Забыл пароль — значит забыл + +Восстановления нет, и это не политика, а криптография: пароль — материал ключа. + +### 5. Оператор не может читать сообщения + +E2EE по умолчанию. У админа с полным доступом к серверу — только шифротекст. + +### 6. Аудируемость через простоту + +Клиент — несколько читаемых файлов без сборки. Любой может проверить глазами, что код делает. + +### 7. Простота важнее функций + +Каждая фича должна оправдать своё существование. По умолчанию — нет. + +## Non-goals + +Это не «пока не успели», а осознанные решения не делать: + +- Облачная синхронизация истории между устройствами. +- «Удалить у всех» после доставки. +- Серверный поиск — поиск только локальный. +- Файлы, медиа, голос, реакции, треды (v1). +- Forward secrecy (v1). +- Федерация. diff --git a/docs/threat-model.md b/docs/threat-model.md new file mode 100644 index 0000000..c75b0c4 --- /dev/null +++ b/docs/threat-model.md @@ -0,0 +1,35 @@ +# Модель угроз + +Честная. Здесь написано не только что Bare защищает, но и чего он защитить не может. + +## Что защищаем + +Содержимое сообщений. Оно шифруется на устройстве отправителя и расшифровывается на устройствах получателей. Сервер, канал и оператор видят только шифротекст. + +## От кого защищаем + +**Пассивный оператор сервера.** Админ с полным доступом к базе и диску видит: ники, argon2-хеши, зашифрованные ключевые блобы, метаданные комнат и контактов, транзитную очередь шифротекстов. Плейнтекста у него нет. + +**Сетевой наблюдатель.** HTTPS обязателен. Наблюдатель видит факт и объём трафика к серверу, не содержимое. + +**Кража базы или бэкапа.** В базе нет ничего сверх того, что видит оператор: те же шифротексты и метаданные. + +**Push-инфраструктура.** Пуш не несёт содержимого: сервер не знает плейнтекста, поэтому его нет и в пуше. + +## От кого не защищаем + +**Активно-злонамеренный оператор.** Оператор, способный подменить клиентский код, может украсть ключи и плейнтекст. Это фундаментальный предел web-E2EE: клиент каждый раз загружается с сервера. Смягчение — открытый код и клиент из нескольких читаемых файлов без сборки: подмену можно заметить глазами. Гарантии нет. + +**Метаданные.** Кто, с кем, когда и сообщениями какого размера обменивается — серверу видно. Скрытие метаданных — не задача Bare. + +**Компрометация устройства.** История лежит на устройстве в открытом виде (IndexedDB). Доступ к устройству — доступ к истории. Защита устройства — зона ответственности пользователя и ОС. + +**Слабый пароль.** Пароль — материал ключа. Ключевой блоб хранится на сервере, и его стойкость к оффлайн-перебору равна стойкости пароля. Слабый пароль ослабляет и вход, и шифрование. + +**Собеседник.** E2EE не защищает от участника чата: получатель может сохранить, переслать, сфотографировать. «Удалить у всех» после доставки не существует. + +## Осознанные пределы v1 + +**Forward secrecy отсутствует.** Компрометация приватного ключа пользователя раскрывает ранее записанные атакующим шифротексты его чатов 1:1. Осознанный non-goal v1. + +**Push-транспорт идёт через инфраструктуру вендоров браузеров** (FCM, APNs, Mozilla). Это свойство стандарта Web Push, а не наша зависимость. Вендоры видят факт и время доставки пуша.