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
+13
View File
@@ -100,6 +100,19 @@ func (c Config) Scheme() string {
return "http"
}
// AllowedOrigins возвращает домены, с которых разрешены WebSocket и
// state-changing запросы (AGENT.md 9.7: точный allowlist, никаких *).
func (c Config) AllowedOrigins() []string {
origins := []string{
"https://" + c.Domain,
"http://" + c.Domain,
}
if c.FilesDomain != "" && c.FilesDomain != c.Domain {
origins = append(origins, "https://"+c.FilesDomain, "http://"+c.FilesDomain)
}
return origins
}
func (c Config) BaseURL() string { return c.Scheme() + "://" + c.Domain }
func (c Config) FilesURL() string { return c.Scheme() + "://" + c.FilesDomain }