Сервер: пакет 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
12 KiB
План реализации 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сам, живой диалог не проверялся. - Прогон сценариев «готово, когда» этапов 1–5 в 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: ни тем, ни аватаров, ни «печатает», ни статусов прочтения.