Заложить репозиторий: философия, архитектура, модель угроз, ADR

Документы фиксируют принятые проектные решения Bare:
клиент без фреймворков и сборки, Go-сервер одним бинарём,
E2EE на WebCrypto, сервер-реле без истории, локальная история
с экспортом/импортом, Web Push + VAPID. Кода нет — сначала документы.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SW4ZXtKn5NjX8NH3Lhm2ND
This commit is contained in:
Claude
2026-08-20 09:17:37 +00:00
co-authored by Claude Fable 5
commit 3d814112a9
20 changed files with 397 additions and 0 deletions
+16
View File
@@ -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.
+15
View File
@@ -0,0 +1,15 @@
# ADR-002: Сервер на Go, один бинарь
## Контекст
Серверу Bare нужно немного: HTTP, SSE, SQLite, хеширование паролей, Web Push. Простота развёртывания и аудита важнее богатства экосистемы.
## Решение
Сервер пишется на Go со стандартной библиотекой. Допущены ровно три внешних пакета: webpush-go, драйвер SQLite, argon2. Результат сборки — один бинарь.
## Следствия
- Развёртывание: скопировать бинарь, запустить. Без рантаймов и обязательных контейнеров.
- Любая новая зависимость требует обсуждения и нового ADR.
- Часть вещей (роутинг, миграции) пишем руками поверх stdlib.
+15
View File
@@ -0,0 +1,15 @@
# ADR-003: SQLite как единственная база
## Контекст
Сервер хранит мало: аккаунты, метаданные комнат и контактов, транзитную очередь. Отдельный сервер БД — лишняя движущаяся часть, противоречащая идее одного бинаря.
## Решение
Вся серверная persistence — SQLite, один файл рядом с бинарём.
## Следствия
- Бэкап — копия одного файла.
- Нет сетевой БД — нет её настройки, аутентификации и отказов.
- Вертикальный потолок масштабирования принимаем осознанно: Bare — маленький инструмент, не платформа.
+15
View File
@@ -0,0 +1,15 @@
# ADR-004: Realtime через SSE и fetch POST
## Контекст
Чату нужна доставка сообщений в реальном времени. WebSocket — отдельный протокол со своим жизненным циклом, капризами прокси и кастомным фреймингом. Хочется остаться в рамках голого HTTP.
## Решение
Приём сообщений — Server-Sent Events. Отправка — обычный `fetch POST`. Чат поверх голого HTTP.
## Следствия
- Никакого кастомного протокола: всё видно в DevTools как обычные HTTP-запросы, реконнект SSE встроен в браузер.
- Работает везде, где работает HTTPS, без апгрейда соединения.
- Двунаправленного канала нет — он и не нужен: у отправки и приёма разные пути.
+16
View File
@@ -0,0 +1,16 @@
# ADR-005: Аккаунт — ник и пароль
## Контекст
Email, телефон и OAuth тянут за собой внешние сервисы, интеграции и утечку идентичности. Bare — независимый инструмент без внешних завязок.
## Решение
Регистрация — ник и пароль. Ник уникален и является идентификатором пользователя. Без email, телефона, OAuth и восстановления пароля. Серверная аутентификация: Argon2id, сессия в httpOnly cookie.
## Следствия
- Ноль внешних сервисов в цикле регистрации и входа.
- Сервер не знает о пользователе ничего, кроме ника.
- Восстановления доступа нет — пароль ещё и материал ключа (ADR-006).
- Открытость регистрации (инвайты или нет) — открытый вопрос.
+16
View File
@@ -0,0 +1,16 @@
# ADR-006: E2EE на WebCrypto, ключ за паролем
## Контекст
Оператор не должен уметь читать сообщения. Крипто-библиотеки на клиенте противоречат нулю зависимостей и аудируемости — вся криптография должна быть нативной.
## Решение
Только WebCrypto. Идентичность пользователя — ECDH-пара P-256. Приватный ключ шифруется ключом, выведенным из пароля (PBKDF2 — единственный KDF в WebCrypto), и хранится на сервере как блоб. Чаты 1:1: ECDH shared secret → AES-GCM.
## Следствия
- Сервер видит только шифротекст — и блоба, и сообщений.
- Сброс пароля невозможен by design: пароль — материал ключа. Забыл — значит забыл.
- Мультидевайс-вход: новый девайс вводит пароль, скачивает блоб, расшифровывает ключ.
- Стойкость блоба к оффлайн-перебору равна стойкости пароля.
+15
View File
@@ -0,0 +1,15 @@
# ADR-007: Симметричный ключ комнаты и rekey
## Контекст
Сообщение в комнате должны читать все участники, но не сервер. Шифровать каждое сообщение отдельно каждому участнику — квадратичный объём работы и трафика.
## Решение
У комнаты один симметричный ключ. Он раздаётся участникам зашифрованным на их публичные ключи. При любом изменении состава — rekey: новый ключ, новая раздача.
## Следствия
- Новый участник не видит сообщений до своего вступления — их и не существует нигде, кроме устройств участников.
- Вышедший участник не читает сообщения после rekey.
- Rekey выполняют клиенты; серверу ключи недоступны.
+15
View File
@@ -0,0 +1,15 @@
# ADR-008: Сервер — реле с per-device очередью
## Контекст
Сервер никогда не является местом, где живёт история (философия, п. 2). Но получатель бывает офлайн — сообщение надо где-то подержать до доставки.
## Решение
Сервер хранит транзитную очередь зашифрованных недоставленных сообщений, per-device. Доставлено и подтверждено ACK — удалено с сервера. Не забрано за 30 дней — удалено. Кроме очереди сервер хранит только аккаунты и метаданные комнат и контактов.
## Следствия
- На сервере нет истории — компрометация сервера не раскрывает переписку.
- Устройство, молчавшее больше 30 дней, теряет недоставленные сообщения. Осознанно.
- Нужна идентификация устройства для очередей — открытый вопрос.
+15
View File
@@ -0,0 +1,15 @@
# ADR-009: История — только на устройстве, в IndexedDB
## Контекст
История, живущая на сервере, делает сервер архивом и целью атак. У Bare история — собственность устройства.
## Решение
Клиент хранит историю в IndexedDB. messageId — ULID/UUIDv7: хронологическая сортировка и идемпотентный merge. Составной индекс (chatId, messageId). Пагинация курсором по ~50 сообщений, виртуализация списка в DOM. При старте — `navigator.storage.persist()`; занятое место показывается через `storage.estimate()`.
## Следствия
- Зашёл с другого устройства — там только новые сообщения.
- Потеря устройства без экспорта — потеря истории.
- Браузер может вычистить storage; `persist()` снижает риск, но не исключает его.
+15
View File
@@ -0,0 +1,15 @@
# ADR-010: Перенос истории — только ручной экспорт/импорт
## Контекст
История живёт на устройстве (ADR-009), но людям нужны перенос на новое устройство и склейка истории с нескольких. Облачная синхронизация сделала бы сервер архивом — non-goal.
## Решение
Экспорт: вся локальная история сериализуется, шифруется отдельной парольной фразой (запрашивается в момент экспорта) и сохраняется одним файлом `.bare`. Импорт: файл плюс фраза, идемпотентный merge в IndexedDB по messageId. Это единственный механизм переноса истории между устройствами.
## Следствия
- Повторный импорт и склейка истории с двух устройств не создают дублей.
- Файл `.bare` самодостаточен и не зависит от пароля аккаунта.
- Перенос — явное действие пользователя. Автоматики не будет. Осознанно.
+15
View File
@@ -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 на экране «Домой», поэтому онбординг-баннер установки — обязательная часть продукта. Разрешение запрашиваем после первого отправленного сообщения, не при входе.
+15
View File
@@ -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.