WEB board

всеканоны и докиворкеры↗ iOS↗ Легаси
WEB-689 · Задача · Инфраструктура · web

P3 [почта]: вебхук Resend — принимать события доставки/отказов с подписью svix

На проверке P3 ведёт: —
Суть
Прод шлёт письма через Resend с 27.09 (отправитель notify@sixbyy.com). События доставки, отказов и жалоб Resend присылает вебхуком с подписью svix (заголовки svix-id, svix-timestamp, svix-signature). Наш /api/email/webhook — это приёмник ВХОДЯЩИХ писем и проверяет простой заголовок x-webhook-secret; с Resend они несовместимы. Нужен отдельный маршрут для событий Resend с проверкой svix (пакет svix уже в lockfile как зависимость resend 6.6.0) и учётом отказов/жалоб, чтобы не слать на мёртвые адреса. Слово владельца 27.09 15:07: «если не срочно но можно доработать то доработайте».
Чем закрывается (приёмка)
Отдельный маршрут принимает события Resend, проверяет svix-подпись и окно времени, fail-closed без секрета в production; неверная подпись 401; повтор того же svix-id не обрабатывается дважды; hard bounce и complaint помечают адрес и дальнейшие письма на него не уходят; тесты зелёные; входящий /api/email/webhook не изменён. Вебхук в кабинете Resend создаёт координатор после посадки.
Доказательства
Resend Webhooks 27.09 13:50Z: пусто. src/app/api/email/webhook/route.ts isAuthorized: x-webhook-secret. pnpm-lock: resend@6.6.0 → svix 1.76.1.
Лента
2026-09-27T15:14:25.515Z · coordinator
[27.09 15:14Z координатор] Взято в работу по слову владельца 27.09 15:07. Волна 5114 (Luna high, Neo) на линии r16 @ 79bbf331: отдельный маршрут событий Resend с проверкой подписи svix, идемпотентность по svix-id, подавление адресов после hard bounce/complaint, тесты. Входящий /api/email/webhook не меняется. Вебхук в кабинете Resend создаю после посадки по рецепту из отчёта.
2026-09-27T15:30:30.688Z · coordinator
[27.09 15:30Z координатор] Волна 5114 (Neo) VERDICT=GO, манифест 45/45 OK. Маршрут /api/email/resend-events: подпись Svix v1 (node-crypto, constant time, окно ±5 мин), без секрета RESEND_WEBHOOK_SECRET в проде 503, неверная подпись 401. Идемпотентность по svix-id, hard bounce и complaint создают suppression, миграции нет. Тесты 156/0, 5 мутаций RED→GREEN, tsc новых ошибок 0. Ветка A2 l115s-web689-resend-events (acc53616 поверх r16 79bbf331). Далее: независимое ревью, сборка r17, затем подключение вебхука в Resend по рецепту из отчёта.
2026-09-27T16:07:36.377Z · coordinator
[27.09 16:07Z координатор] [27.09 17:10Z координатор] Независимое ревью 5117 (M1, gpt-5.6-sol high) VERDICT=GO, evidence SHA256SUMS OK. Найдено и исправлено (тест RED→GREEN):
- WEB-689 HIGH: жёсткий bounce Resend приходит как bounce.type="Permanent", а код подавлял адрес только для "hard" → адрес не блокировался. Исправлено.
- WEB-689 MEDIUM: секрет/подпись svix в base64 принимались с мусором в конце. Теперь только канонический base64.
- WEB-573 HIGH: ответ {type:"google"} без остальных полей считался валидным и превращался в «Не подключено». Исправлено: минимально валидная запись сохраняется, неполная = ошибка.
PASS: HMAC по сырому телу, timingSafeEqual, несколько v1, окно ±300 с, fail-closed без секрета, 401 на неверной подписи, идемпотентность по svix-id (гонка), complaint/Permanent блокируют sendEmail до транспорта, неизвестный тип 2xx; логи без адресов/секретов; новой миграции нет. Один парсер ответа подключений и один UI-потребитель.
Итог тестов: 83/83 focused, scoped tsc RC0, adversarial RED подтверждён.
ОСТАТОК (LOW): у нового подписанного маршрута общий долг rate-limit/monitoring API-периметра.
КОД: A2 nc-build ветка integrate/5118-r17 = 1f78507e (r16 79bbf331 + 7 коммитов). Патчи ревью: m1 ~/waves/5117REVIEWR17-evidence/000[1-5]*.patch.
ДАЛЬШЕ: сборка r17 = волна 5118 (A2, стартует после деплоя r16 5115) → деплой на стенд → посадка. После посадки: создать вебхук в панели Resend на /api/email/webhook/resend (секрет whsec_ в env прода) и живой вход Google.
Воркер
не привязан — привязать: curl -X POST https://bugs.wool2.online/api/web/assign -H 'content-type: application/json' \ -d '{"issueId":"WEB-689","session":"<имя tmux-сессии>","host":"m4"}'
Обновлён
2026-09-27T15:30:31.102Z