Files
bare/docs/plan.md
T
mayatnikovandClaude Opus 5 8f67f4aa4d Этап 4: PWA и пуши — service worker, Web Push с VAPID, баннер установки
Сервер: пакет push на webpush-go (третья и последняя прямая зависимость),
подписка устройства, правило ADR-023 «пуш только молчащему устройству и только
один» через атомарный захват push_pending, удаление подписки на 404 и 410,
TTL сутки, urgency normal. В нагрузке только {title, body, chat} — тело
собирается из константы, плейнтекст туда не попадает даже по ошибке.

Клиент: service worker с версионированным кэшем оболочки и никогда — /api/*,
push и notificationclick, подписка на VAPID-ключ сервера, запрос разрешения
после первого отправленного сообщения, разделы настроек «уведомления»
и «установить приложение», баннер установки на iOS.

ADR-045: пуш адресован получателю — по букве ADR-023 он уходил бы и молчащему
устройству отправителя с бессмысленным заголовком из собственного ника.
ADR-047: сервер ходит на endpoint подписки, который выбирает браузер. Проверка
«только https» обходилась редиректом, а имя могло смотреть внутрь сети — теперь
запрет редиректов и проверка разрешённого адреса на уровне сокета.
ADR-048: пределы отправки — недоступный push-сервис одного аккаунта больше
не съедает пуши всего сервера.
ADR-049: явно выключенные уведомления сами не включаются обратно.

Попутно: webpush-go дописывает набивку в переданный срез, а одна нагрузка
уходила всем устройствам сообщения — гонка, пойманная go test -race.
Теперь у каждого задания своя копия.

Приёмка на боевом: подписки, hasPush, чужое устройство, Origin, оболочка
из девяти файлов, Service-Worker-Allowed. Отдельно шесть непубличных адресов
и endpoint на 3 КиБ — все отбиты.

Чеклист ручной проверки на iPhone и Android — в docs/plan.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015DbCjVfTFq4ZFG8juD45YJ
2026-08-23 00:50:58 +03:00

12 KiB
Raw Blame History

План реализации v1

Документ для исполнителя — человека или агента. Всё, что здесь, выводится из ADR и спецификаций; при расхождении правы ADR. Этапы идут по порядку, каждый заканчивается работающим деплоем на bare.xmatic.team и коммитом.

Источники истины

вопрос документ
что и почему philosophy.md, architecture.md, threat-model.md, decisions/
криптография, байт в байт crypto.md
HTTP-API, SSE, коды ошибок protocol.md
схема SQLite, IndexedDB, формат .bare storage.md
экраны, тексты, поведение ui.md, identity/
сервер, nginx, systemd deploy.md

Раскладка репозитория

embed.go                         //go:embed web в корне модуля (ADR-025)
cmd/bare/main.go                 подкоманды: serve, vapid, version
internal/config/                 переменные BARE_*
internal/store/                  SQLite, migrations/*.sql (embed), запросы
internal/auth/                   argon2id, сессии, cookie
internal/hub/                    SSE-соединения по deviceId
internal/push/                   webpush-go, правила ADR-023
internal/api/                    маршруты, валидация, лимиты, заголовки
internal/web/                    embed web/, отдача статики
web/
  index.html  app.css  manifest.json  sw.js
  icons/                         уже в репозитории
  js/main.js                     загрузка, роутинг, состояние
  js/api.js                      fetch-обёртки, SSE, ACK
  js/crypto.js                   всё из crypto.md
  js/db.js                       IndexedDB из storage.md
  js/sync.js                     устройство, поток событий, приём и отправка
  js/pwa.js                      service worker, подписка на пуши, установка
  js/ulid.js                     ULID
  js/ui/*.js                     экраны из ui.md
  js/export.js                   .bare
scripts/deploy.sh

Go — последняя стабильная версия, маршрутизация net/http с шаблонами методов ("POST /api/messages"). Прямые зависимости ровно три (ADR-020). Клиент — ES-модули, без сборки, без inline-стилей и скриптов, innerHTML запрещён.

Этап 0 — скелет и деплой

  • go mod init, cmd/bare, serve слушает BARE_ADDR, отдаёт web/ из embed, /healthz, заголовки безопасности.
  • web/index.html — страница со знаком и словом bare, app.css с переменными из identity/brief.md, manifest.json, пустой sw.js с версией.
  • scripts/deploy.sh; на сервере — пользователь, каталоги, env, юнит, nginx, сертификат по deploy.md.

Готово, когда https://bare.xmatic.team/ открывается с правильным CSP, /healthz отвечает ok, journalctl -u bare чист.

Этап 1 — аккаунты

  • Миграция 001, store с user_version, фоновая чистка.
  • auth: argon2id с параметрами ADR-021, сессии, cookie, проверка Origin.
  • Эндпоинты: config, kdf, register, login, logout, me, password, DELETE /api/me, users/{nick}.
  • Клиент: crypto.js (мастер, authKey, kek, блоб, ключевая пара, отпечаток), экран входа и регистрации, сохранение CryptoKey в IndexedDB, автоповышение итераций, настройки с «сменить пароль» и «выйти».
  • Тесты Go: миграции на пустой базе, регистрация и вход, неверный authKey, смена пароля с logoutOthers.

Готово, когда регистрация и вход работают на телефоне и десктопе, вход на втором устройстве расшифровывает тот же ключ (отпечатки совпадают), пароль в сетевых запросах не встречается.

Этап 2 — чат 1:1

  • devices, hub, queue, POST /api/messages, ack, events с воспроизведением очереди и пингом.
  • Клиент: ulid.js, db.js, api.js с SSE и ACK после записи, шифрование сообщений, экран чата (десктоп и мобильный по эталону), список чатов, «новый чат», разделители дат и «новые», pending/failed, повтор после реконнекта.
  • Контакты: GET/POST/DELETE /api/contacts, автосоздание при первом сообщении.
  • Тесты Go: фан-аут по устройствам без эха отправителю, ACK удаляет, повтор очереди при реконнекте, clock_skew, лимит 30/мин.

Готово, когда два аккаунта переписываются в реальном времени, второе устройство получателя получает копию, офлайн-устройство получает очередь при открытии, в базе — только шифротекст.

Этап 3 — ключи и комнаты

  • TOFU: хранилище peers, карточка контакта, предупреждение о смене ключа, «доверять новому ключу», повторная расшифровка raw.
  • Комнаты: rooms и room_keys, все эндпоинты из protocol.md, события room/room_left, передача владения, rekey при выходе.
  • Клиент: создание комнаты, участники, заворачивание и разворачивание ключей, хранение roomKeys, отправка с текущим keyId, расшифровка любым известным.
  • Тесты Go: keys_mismatch, key_exists, выход владельца, удаление пустой комнаты, обрезка ключей до двух.

Готово, когда трое переписываются в комнате, добавленный четвёртый читает только новое, вышедший не получает новых сообщений после rekey, подмена public_key в базе вручную вызывает предупреждение у собеседника.

Этап 4 — PWA и пуши

  • sw.js: кэш оболочки, push, notificationclick; manifest.json с иконками; apple-touch-icon.
  • PUT/DELETE /api/devices/{id}/push, отправка по правилам ADR-023, обработка 404/410.
  • Клиент: запрос разрешения после первого сообщения, настройки уведомлений, баннер установки на iOS, beforeinstallprompt.

Готово, когда закрытое PWA на iPhone и Android получает пуш и открывается на нужном чате; повторные сообщения до открытия пуш не порождают.

Чеклист ручной проверки на устройствах

Автоматически проверено всё, что проверяется без настоящих устройств: правило «одно молчащее устройство — один пуш», сброс push_pending при подключении SSE, удаление подписки на 404/410, расшифровка пуша по RFC 8291 в тесте, отсутствие плейнтекста в нагрузке, кэш оболочки без /api/*, отказ ходить на непубличные адреса. Осталось то, что требует рук и телефона:

  • iPhone, установленное на «Домой» приложение: пуш приходит при закрытом приложении, нажатие открывает нужный чат.
  • Android Chrome, закрытое приложение: то же самое.
  • Клик по системному уведомлению в обоих случаях: в уже открытое окно (фокус и переход) и при закрытом приложении (/#/dm/<nick>, /#/room/<id>).
  • Повторные сообщения до открытия: второе и третье пуша не порождают.
  • iOS вне PWA: баннер установки над списком чатов, текст, крестик и то, что он больше не появляется; в настройках «уведомления» — текст про установку вместо кнопки.
  • beforeinstallprompt в обычном Chrome: раздел «установить приложение» появляется, после нажатия исчезает целиком.
  • Системный запрос разрешения после первого отправленного сообщения: headless-Chrome отвечает denied сам, живой диалог не проверялся.
  • Прогон сценариев «готово, когда» этапов 15 в Safari (iOS и десктоп) и Firefox — автоматика гоняла только Chrome.

Этап 5 — история

  • Экспорт и импорт .bare по crypto.md и storage.md; идемпотентность; «архив создан другим аккаунтом».
  • Настройки: устройства (список, удаление), занятое место, удаление аккаунта.
  • Пагинация ленты по 50 с подгрузкой вверх.

Готово, когда экспорт с одного устройства и импорт на другом дают одинаковую ленту без дублей, повторный импорт ничего не меняет, чужой архив отклоняется до расшифровки.

Этап 6 — закалка

  • Rate limiting по всем правилам ADR-021, 413, 429 с Retry-After.
  • Проверка CSP в консоли браузера: ноль нарушений.
  • Прогон модели угроз по коду: пароль не уходит, from ставит сервер, Origin проверяется, cookie с нужными флагами.
  • Синхронизация документов с кодом: расхождение — правка документа через ADR или правка кода.

Определение готовности v1

Все шесть этапов; тесты Go зелёные; ручной прогон сценариев из каждого «готово, когда» на iOS Safari (PWA), Android Chrome, десктопных Chrome, Firefox, Safari; README.md обновлён со статуса «проектирование».

Правила для исполнителя

  • Сомнение в спецификации — сначала ADR, потом код. Не дописывать спецификацию молча.
  • Новая зависимость, новый эндпоинт, новое поле в конверте — только через ADR.
  • Каждый этап — отдельный коммит или серия коммитов с деплоем; не копить.
  • Не добавлять фич сверх ui.md: ни тем, ни аватаров, ни «печатает», ни статусов прочтения.