feat(voice): вебхуки LiveKit и сторож голосовых состояний

AGENT.md 7.14:

- POST /api/v1/livekit/webhook: подпись проверяется JWT-токеном, подписанным
  API-секретом, с проверкой sha256 тела (иначе 401), при выключенном голосе —
  503; обработка идемпотентна по event.id (повторная доставка → duplicate);
- события: participant_joined восстанавливает состояние после рестарта
  приложения, participant_left и participant_connection_aborted убирают
  участника, room_finished очищает комнату целиком, track_published и
  track_unpublished обновляют флаги камеры и демонстрации экрана; каждое
  изменение рассылает VOICE_STATE_UPDATE участникам сервера;
- сторож `StartVoiceWatchdog` раз в 15 минут удаляет состояния без обновлений
  дольше шести часов (сбои без вебхука) и уведомляет клиентов;
- тесты: подпись (чужой секрет, другое тело, отсутствие заголовка), полный
  цикл событий SFU и идемпотентность повторной доставки.
This commit is contained in:
2026-09-20 18:31:51 +03:00
parent 2749a0e831
commit e9455ab6fd
8 changed files with 621 additions and 5 deletions
+31
View File
@@ -89,3 +89,34 @@ func TestResolveLiveKitURL(t *testing.T) {
t.Fatal("http-адрес должен отклоняться")
}
}
func TestVerifyWebhookSignature(t *testing.T) {
issuer := voice.NewIssuer("key123", "secret456", time.Hour)
body := []byte(`{"event":"participant_joined"}`)
// Без заголовка — отказ.
if err := issuer.VerifyWebhook("", body); err == nil {
t.Fatal("подпись обязательна")
}
// Чужой подписью — отказ.
other := voice.NewIssuer("key123", "another-secret", time.Hour)
token, err := other.IssueWebhookForTest(body)
if err != nil {
t.Fatalf("issue: %v", err)
}
if err := issuer.VerifyWebhook(token, body); err == nil {
t.Fatal("подпись чужим секретом должна отклоняться")
}
// Своей подписью, но другим телом — отказ.
valid, err := issuer.IssueWebhookForTest(body)
if err != nil {
t.Fatalf("issue: %v", err)
}
if err := issuer.VerifyWebhook(valid, []byte(`{"event":"room_finished"}`)); err == nil {
t.Fatal("несовпадение тела должно отклоняться")
}
// Корректная подпись — успех.
if err := issuer.VerifyWebhook(valid, body); err != nil {
t.Fatalf("VerifyWebhook: %v", err)
}
}