Сервер: пакет 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
138 lines
12 KiB
Markdown
138 lines
12 KiB
Markdown
# План реализации 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`: ни тем, ни аватаров, ни «печатает», ни статусов прочтения.
|