feat(gateway): WebSocket Gateway с HELLO, IDENTIFY и READY

Реализован Gateway по AGENT.md §8.3:

- оп-коды 0/1/2/3/4/7/10/11, единый конверт {op,t,d,s};
- HELLO с интервалом сердцебиения (45 с) и идентификатором сессии;
- IDENTIFY/RESUME с проверкой токена через auth.ResolveSession;
- READY: пользователь, серверы, видимые комнаты, роли, участники и права,
  собранные через движок прав (VIEW_CHANNEL-фильтрация);
- HEARTBEAT → HEARTBEAT_ACK, лимит частоты IDENTIFY (5 с);
- RESUME: буфер на 1000 событий с TTL 5 минут, догон пропущенных событий и
  подтверждение RESUMED, полный READY при расхождении состояния;
- рассылка Dispatch/SendToUser с буферизацией для отключённых сессий;
- проверка Origin по allowlist доменов инстанса (AGENT.md §9.7);
- httpx.statusRecorder пробрасывает Hijack, иначе апгрейд ломается.

Тесты: 6 сценариев Gateway (READY, скрытые комнаты, неверный токен,
heartbeat, dispatch, RESUME) и 2 теста маршрута /gateway через полный стек
middleware, включая отклонение чужого Origin.
This commit is contained in:
2026-09-19 21:42:41 +03:00
parent c0537d3c17
commit 86dff94a02
11 changed files with 1364 additions and 18 deletions
+15
View File
@@ -1,6 +1,7 @@
package httpx
import (
"bufio"
"context"
"log/slog"
"net"
@@ -44,6 +45,20 @@ func (r *statusRecorder) Flush() {
}
}
// Unwrap отдаёт исходный ResponseWriter: http.ResponseController и другие
// обёртки должны видеть возможности базового писателя.
func (r *statusRecorder) Unwrap() http.ResponseWriter { return r.ResponseWriter }
// Hijack пробрасывает перехват соединения: без него не работает апгрейд на
// WebSocket через /gateway (AGENT.md 8.3).
func (r *statusRecorder) Hijack() (net.Conn, *bufio.ReadWriter, error) {
hijacker, ok := r.ResponseWriter.(http.Hijacker)
if !ok {
return nil, nil, http.ErrNotSupported
}
return hijacker.Hijack()
}
// RequestInfoMiddleware кладёт IP и User-Agent запроса в контекст: huma-хендлеры
// не получают *http.Request, а сервисам эти данные нужны (аудит, безопасность).
func RequestInfoMiddleware(next http.Handler) http.Handler {