Закрыть открытые вопросы: ADR-015…022, бриф айдентики

Регистрация открытая (015), контакты и приглашения по нику с
закреплением ключа и блокировкой (016), лимиты (017), идентификация
устройства через случайный deviceId в IndexedDB и серверный fan-out
(018), отказ от серверного перца (019), смена пароля не в v1 (020),
интерфейс ru/en (021), айдентика «Скобы» по turn 2 канваса (022).

Архитектура, модель угроз и README обновлены. В открытых вопросах
остались два: моногарнитура с кириллицей (Fragment Mono её не
содержит) и удаление аккаунта.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R2pCJkWyG2aYu48u1Vp1A1
This commit is contained in:
2026-08-20 22:42:20 +03:00
co-authored by Claude Fable 5
parent b9bf4ea04e
commit a898347005
15 changed files with 290 additions and 14 deletions
+1 -1
View File
@@ -13,4 +13,4 @@ Email, телефон и OAuth тянут за собой внешние сер
- Ноль внешних сервисов в цикле регистрации и входа.
- Сервер не знает о пользователе ничего, кроме ника.
- Восстановления доступа нет — пароль ещё и материал ключа (ADR-006).
- Открытость регистрации (инвайты или нет) — открытый вопрос.
- Регистрация открытая — ADR-015.
+1 -1
View File
@@ -12,4 +12,4 @@
- На сервере нет истории — компрометация сервера не раскрывает переписку.
- Устройство, молчавшее больше 30 дней, теряет недоставленные сообщения. Осознанно.
- Нужна идентификация устройства для очередей — открытый вопрос.
- Идентификация устройства для очередей — ADR-018.
+17
View File
@@ -0,0 +1,17 @@
# ADR-015: Регистрация открытая
Закрывает открытый вопрос из [ADR-005](005-accounts.md).
## Контекст
ADR-005 оставил открытым, нужны ли инвайты. Инвайты — это сущность (код, выдача, срок, учёт), отдельный UI и барьер для первого контакта с продуктом. Bare — публичный инструмент, а не закрытый клуб.
## Решение
Регистрация открытая: ник и пароль, больше ничего. Инвайтов нет.
## Следствия
- Любой может завести аккаунт, значит, любой может злоупотреблять. Защита — не на входе, а в механике общения: сообщения ходят только между взаимными контактами (ADR-016) и в пределах лимитов (ADR-017).
- Ники занимаются по принципу «кто первый». Споров о никах сервер не разбирает.
- Закрытая регистрация для приватных инсталляций — отдельным ADR, если понадобится. В v1 её нет.
+24
View File
@@ -0,0 +1,24 @@
# ADR-016: Контакты и приглашения — по нику
Закрывает открытый вопрос о механике добавления контакта.
## Контекст
Ник — единственный идентификатор пользователя (ADR-005). Ссылки-приглашения, QR-коды, поиск и каталог пользователей — отдельные сущности со своим жизненным циклом. При открытой регистрации (ADR-015) нужен ещё и механизм согласия: иначе любой, кто знает ник, может писать кому угодно.
## Решение
**Контакт.** Пользователь вводит точный ник. Сервер отдаёт публичный ключ и создаёт запрос контакта. Адресат видит запрос и принимает или отклоняет. Сообщения 1:1 ходят только между взаимными контактами. Поиска по части ника и каталога нет.
**Ключи.** Публичный ключ контакта сохраняется на устройстве при первом получении и больше с сервера не перечитывается. Если сервер когда-либо вернёт для этого ника другой ключ — клиент показывает предупреждение и не шифрует на новый ключ без явного подтверждения. Отпечаток ключа виден в карточке контакта; сверка вне канала — по желанию пользователя, автоматики нет.
**Комната.** Участник приглашает в комнату только своих контактов — добавлением в состав, без отдельного согласия: согласие уже дано на уровне контакта. Не хочешь — выходишь. Создатель комнаты может исключать участников. Любое изменение состава — rekey (ADR-007).
**Блокировка.** Отклонить запрос можно с блокировкой ника: сервер не доставляет от заблокированного ни сообщений, ни новых запросов.
## Следствия
- Серверу достаточно таблиц контактов (пара ников, статус) и блокировок. Новых сущностей — инвайтов, ссылок, токенов — нет.
- Ник надо знать заранее и передать вне Bare. Это ограничение осознанное: Bare — инструмент для людей, которые уже знакомы.
- Закрепление ключа защищает от подмены ключа сервером после первого контакта. Первый контакт — доверие на слово (TOFU): активно-злонамеренный оператор в модели угроз и так за пределами защиты.
- Спам ограничен структурно: чужому нельзя написать, пока он не принял запрос; сами запросы — под лимитами (ADR-017).
+34
View File
@@ -0,0 +1,34 @@
# ADR-017: Лимиты
Закрывает открытый вопрос о лимитах и антиспаме.
## Контекст
Открытая регистрация (ADR-015) и реле с очередями (ADR-008) требуют потолков: на размер сообщения, на частоту действий, на объём хранимого. Сервер не видит содержимого, поэтому антиспам может быть только структурным и количественным — фильтрации по тексту не существует.
## Решение
**Размеры.**
- Сообщение — до 4 096 символов плейнтекста; проверяет клиент до шифрования.
- Тело любого запроса к серверу — до 32 КиБ. Это единственная проверка размера на сервере.
- Ник — 3–32 символа: латиница в нижнем регистре, цифры, `_`, `-`. Регистр не различается.
**Частота.** Token bucket в памяти сервера, без внешних хранилищ. Превышение — `429` с `Retry-After`.
- Отправка сообщений: 60 в минуту на аккаунт.
- Запросы контакта: 20 в сутки на аккаунт.
- Регистрация: 5 в час с одного IP.
- Вход: 10 попыток за 15 минут на ник, 30 — с одного IP. Argon2 дорогой; это защита процессора сервера в той же мере, что и аккаунтов.
- Прочие запросы: 120 в минуту на аккаунт.
**Объёмы.**
- Очередь устройства — до 10 000 сообщений; при переполнении старейшие удаляются. Плюс 30-дневный TTL из ADR-008.
- Контактов у аккаунта — до 500. Участников в комнате — до 100. Устройств у аккаунта — до 10.
**Антиспам.** Сводится к трём вещам: писать можно только взаимным контактам (ADR-016), блокировка ника (ADR-016), лимиты выше. Жалоб и модерации нет: серверу нечего модерировать.
## Следствия
- Все числа — константы в одном файле сервера. Клиент дублирует только длину сообщения и формат ника — для подсказок до отправки. Конфигурации нет: поменять лимит — поменять константу и пересобрать.
- Перезапуск сервера сбрасывает счётчики. Для маленького инструмента это приемлемо.
- Лимит по IP не различает людей за одним NAT. Осознанная грубость: цифры взяты с запасом.
- Переполнение очереди теряет сообщения, как и TTL. Устройство, не выходившее на связь, получает не всё — это уже зафиксировано в ADR-008.
+22
View File
@@ -0,0 +1,22 @@
# ADR-018: Устройство — случайный идентификатор в IndexedDB
Закрывает открытый вопрос из [ADR-008](008-server-relay.md).
## Контекст
Очередь недоставленных сообщений — per-device (ADR-008), подписка на пуши — тоже per-device (ADR-011). Серверу нужно отличать устройства одного аккаунта. Стабильного идентификатора браузера не существует, и это правильно; его надо завести самим.
## Решение
- При первом запуске клиент генерирует 128-битный случайный `deviceId` (`crypto.getRandomValues`) и хранит его в IndexedDB рядом с историей. Одно хранилище — один жизненный цикл: стёрли данные сайта — исчезли и история, и идентичность устройства. Новый запуск — новое устройство.
- При входе клиент передаёт `deviceId`. Сервер создаёт запись устройства с ключом (аккаунт, `deviceId`) и очередь к нему. Сессионная cookie привязана к устройству.
- `deviceId` — не секрет, а адрес очереди. Аутентифицирует сессия. Коллизии между аккаунтами невозможны: ключ составной.
- Доставка — серверный fan-out: сообщение раскладывается в очереди всех устройств всех участников, кроме отправившего. Отправитель не знает и не должен знать устройств адресата: все устройства одного аккаунта владеют одним приватным ключом (ADR-006), шифротекст один для всех.
- Устройство без связи 30 дней удаляется вместе с очередью и push-подпиской — тот же срок, что TTL очереди (ADR-008). На следующем запуске клиент регистрируется как новое устройство; локальная история при этом цела.
- Клиент при регистрации устройства передаёт короткую метку из user-agent («Firefox · Linux»). Список устройств с метками и датой последней связи виден в настройках; любое можно удалить — очередь и сессия уничтожаются.
## Следствия
- Сообщение, отправленное с телефона, появляется и на ноутбуке — как новое, через его очередь. Это не синхронизация истории: то, что было до появления устройства, на него не приедет.
- Один браузер в двух профилях или в режиме инкогнито — разные устройства. Инкогнито плодит устройства при каждом запуске; их чистит 30-дневный срок и лимит в 10 устройств (ADR-017).
- Потеря `deviceId` (чистка storage) эквивалентна потере устройства: недоставленное в старую очередь пропадает по TTL. `navigator.storage.persist()` (ADR-009) снижает риск.
+23
View File
@@ -0,0 +1,23 @@
# ADR-019: Серверного «перца» для ключевого блоба нет
Закрывает открытый вопрос о дополнительном шифровании блоба серверным ключом.
## Контекст
Идея: шифровать ключевой блоб ещё раз серверным ключом, хранящимся вне базы. Тогда украденный дамп или бэкап сам по себе не даёт материала для оффлайн-перебора паролей. Против оператора это не помогает — у него есть оба.
## Решение
Перца нет. Блоб хранится так, как описано в ADR-006 и ADR-013: один слой шифрования, ключ выведен из пароля.
Аргументы:
- Появляется второй критичный артефакт рядом с базой. ADR-003 обещает «бэкап — копия одного файла»; перец это обещание ломает. Потеря файла с перцем — невозможность входа с новых устройств для всех пользователей сразу, а восстановления паролей в Bare нет by design. Для маленького самохостного инструмента риск потерять ключ при переезде выше риска, который он закрывает.
- Сценарий «утёк бэкап, но не сервер» уже закрыт ADR-013: PBKDF2 с миллионом итераций и пароль от 12 символов делают перебор дорогим. Словарный пароль перец тоже не спасёт при компрометации сервера.
- Каждый дополнительный слой — код, который нужно аудировать, и оперативная процедура, которую нужно помнить. Простота важнее.
## Следствия
- Стойкость блоба в любом сценарии равна стойкости пароля, как и записано в модели угроз. Новых оговорок в ней не появляется.
- Развёртывание остаётся «бинарь плюс файл базы». Ничего, что нельзя потерять, кроме самой базы, у оператора нет.
- Решение обратимо: перец можно добавить позже отдельным ADR — он не меняет формат блоба, только оборачивает его.
@@ -0,0 +1,18 @@
# ADR-020: Смена пароля — не в v1
Закрывает открытый вопрос о сроках смены пароля.
## Контекст
Пароль — материал ключа, которым зашифрован ключевой блоб (ADR-006). Смена пароля — это расшифровка блоба старым ключом и перешифровка новым, целиком на клиенте; ECDH-пара и секрет аккаунта не меняются. Технически просто, но это отдельный сценарий с гонками между устройствами и отдельный UI.
## Решение
В v1 смены пароля нет. Появится позже отдельным ADR.
Формат блоба уже готов к этому: параметры KDF лежат рядом с блобом (ADR-013), тот же механизм перешифровки нужен для повышения итераций. Ключи и секрет аккаунта при смене пароля сохраняются, поэтому контакты, комнаты и экспортированные архивы останутся в силе.
## Следствия
- В v1 скомпрометированный пароль — скомпрометированный аккаунт, и путь один: новый аккаунт. Об этом надо прямо сказать при регистрации, рядом с «восстановления нет».
- Серверная часть смены пароля — замена argon2-хеша и блоба одним запросом — тривиальна; откладывается целиком, чтобы не делать половину.
+21
View File
@@ -0,0 +1,21 @@
# ADR-021: Интерфейс на русском и английском, i18n своими руками
Закрывает открытый вопрос о языке интерфейса.
## Контекст
Аудитория Bare двуязычна. Библиотеки i18n на клиенте невозможны (ADR-001), и они не нужны: интерфейс чата — несколько десятков строк.
## Решение
- Два языка: русский и английский. Других в v1 нет.
- Все строки интерфейса — в одном ES-модуле, два плоских словаря с одинаковыми ключами. Ничего сверх: никаких форматов файлов переводов, никаких внешних инструментов.
- Язык при первом запуске — из `navigator.language` (ru → русский, всё остальное → английский). Переключатель в настройках, выбор хранится в `localStorage`.
- Склонения и множественные формы — руками, через `Intl.PluralRules`. Даты и время — через `Intl.DateTimeFormat` с выбранной локалью.
- Документация остаётся на русском.
## Следствия
- Добавить язык — добавить словарь; удалить — удалить. Ключи проверяются на полноту простым скриптом при желании, не сборкой.
- Ники и содержимое сообщений языком интерфейса не затрагиваются.
- Текст в UI короткий и строчный — по голосу бренда (ADR-022). Переводить надо в том же регистре и тоне.
+21
View File
@@ -0,0 +1,21 @@
# ADR-022: Визуальная айдентика — «Скобы», моно-интерфейс без баблов
Закрывает открытый вопрос о визуальной айдентике.
## Контекст
Айдентика исследовалась в Claude Design: шесть направлений знака, два развиты до мини-айдентик, затем интерфейс чата на базе одной из них. Исследование — [канвас «Исследование айдентики Bare»](https://claude.ai/design/p/0126e4da-40e4-449c-bfbb-7aefc7b64659?file=Bare+Identity.dc.html).
## Решение
Работаем с turn 2 канваса: интерфейс на базе мини-айдентики 1h «Скобы», одна гарнитура — моноширинная, сообщения без баблов. Артборды 2a (десктоп, 1120) и 2b (мобильный, 390) — референс для вёрстки клиента.
Подробности — палитра, типографика, правила знака, структура экрана — в [docs/identity/brief.md](../identity/brief.md). Бриф — источник истины для клиента; канвас — история исследования.
## Следствия
- Знак — четыре угла рамки, внутри пусто. Без градиентов, теней, маскотов и пузырей.
- Цвет акцента — только для скоб в акцентных случаях и статусов. Не для кнопок и заливок.
- Сообщения — строки: автор, время, текст. Эстетика лога, а не мессенджера.
- Шрифт в продукте не загружается с внешних хостов: либо файл в репозитории, либо системный моно-стек. Внешний запрос за шрифтом — утечка метаданных и зависимость, несовместимые с философией.
- Выбор конкретной моногарнитуры для кириллицы — открытый вопрос (см. бриф).