POST/DELETE /internal/jitsi/reservations — Резервирование медиа-комнаты (prosody admission)
Граница интеграции media-стека: prosody mod_reservations спрашивает messenger, можно ли поднять
MUC-комнату по имени, а при уничтожении комнаты — уведомляет об этом. Это единственный авторитет допуска
комнат: комната существует только если её имя совпадает с RoomName живой сессии PIN
(call-session.md / conference-session.md),
провизированной бэкендом. Самоподнятых комнат в развёртывании нет.
Граница именования. Это внутренний вебхук от media-стека — имена Jitsi/prosody/mod_reservations
здесь предметны и намеренны. Наружу (в api/calls/api/conferences/домене) media-стек описывается
нейтрально (SFU, ADR-0062).
Назначение
mod_reservations вызывает POST /internal/jitsi/reservations перед созданием MUC-комнаты и
DELETE /internal/jitsi/reservations/{roomName} при её уничтожении. На POST messenger разрешает или
запрещает комнату: имя должно точно совпадать с Room.RoomName живой (входибельной) сессии; при разрешении
возвращается дескриптор JitsiReservationModel (лимиты, флаг записи), по которому prosody конфигурирует
комнату. Запрет → prosody отклоняет создание комнаты. DELETE — сигнал опустошения комнаты: участники
помечаются вышедшими, финальное завершение сессии отдаётся reaper-у.
| Поле | Значение |
|---|
| Сервис/контекст | messenger |
| API area | internal (вебхук media-стека) |
| Feature group | media (jitsi-интеграция) |
| Статус | draft |
| Документ | docs/06-services/messenger/api/internal/jitsi-reservation.md |
Актор и доступ
| Проверка | Значение |
|---|
| Actor type | ExternalSystem — prosody mod_reservations (не OIDC-клиент) |
| Auth policy / Permissions | подписанный вебхук: заголовок X-Pin-Signature = HMAC над сырым телом + X-Pin-Timestamp, окно replay ≤ 5 мин, общий секрет; либо mTLS. Пользовательских прав нет (m2m) |
| Scope (RBAC) | не применяется (доверенная граница инфраструктуры) |
| Record-level | доступ по совпадению Name с Room.RoomName конкретной сессии — сам предмет проверки (fail-closed) |
| Tenant isolation | не применяется; сессия резолвится по имени комнаты, не по TenantId |
| Audit | update: провизия/завершение сессии по admission — иммутабельные фазовые метки сессии |
| Trusted boundary | вход подписан/mTLS, но валидируется полностью: подпись → парсинг имени → совпадение RoomName → допустимость состояния |
HTTP-контракт
| Поле | Значение |
|---|
| Method | POST (allow-решение) / DELETE (room destroyed) |
| Route | POST /api/messenger/internal/jitsi/reservations ; DELETE /api/messenger/internal/jitsi/reservations/{roomName} |
| Success status | POST → 200 (allow, тело JitsiReservationModel) / 403,409 (deny) ; DELETE → 200 (ack) |
| Idempotency | по RoomName (сессии): повтор POST на входибельной сессии отдаёт тот же дескриптор; повтор DELETE/на терминале — no-op ack |
| Correlation | сквозной trace через traceparent |
Входные данные / параметры запроса
POST — JitsiReservationRequest
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|
X-Pin-Signature | header | string | Да | HMAC над сырым телом; несовпадение → MSG_JITSI_SIGNATURE_INVALID | секрет не логируется |
X-Pin-Timestamp | header | DateTimeOffset | Да | окно replay ≤ 5 мин | — |
Name | body | string | Да | MUC-localpart комнаты; из него парсится sessionId + nonce; шаблон Room.RoomName; несовпадение → MSG_JITSI_ROOM_UNKNOWN | маршрутный id, не PII |
MucRoomJid | body | string | Да | полный JID комнаты (Name@conference.<domain>) | — |
StartTime | body | DateTimeOffset? | Нет | заявленное время старта комнаты (эхо в ответ) | — |
MailOwner | body | string? | Нет | владелец из токена создателя (может отсутствовать) | не используется как доступ |
RoomParam | body | string? | Нет | доп. параметры от prosody (игнорируются, если не распознаны) | — |
DELETE — параметр маршрута
| Поле | Источник | Тип | Обязательность | Валидация / Ограничения | Безопасность |
|---|
X-Pin-Signature / X-Pin-Timestamp | header | — | Да | как выше | — |
roomName | route | string | Да | Room.RoomName уничтожаемой комнаты; неизвестное имя → тихий 200 ack (комнаты уже нет — нечего закрывать) | — |
Модель ответа JitsiReservationModel (только POST, allow)
Дескриптор допуска комнаты для prosody. Recording — единственное производное решение: включён ли для сессии
захват (есть активная запись). Ссылок/токенов/медиа в ответе нет.
| Поле | Тип | Обязательность | Источник | Masking | Описание |
|---|
Id | string | Да | sessionId (звонок/конференция) | — | Идентификатор сессии PIN, к которой привязана комната. |
Name | string | Да | Room.RoomName | — | Подтверждённое имя комнаты (эхо). |
StartTime | DateTimeOffset | Да | сессия / запрос | — | Время старта комнаты. |
Duration | int? | Нет | policy-cap | — | Предельная длительность комнаты в секундах (после — reaper закрывает). |
Recording | bool | Да | наличие CallRecordingEntity в Pending для сессии | — | Нужно ли поднимать jibri (авто-запись по политике). |
MaxOccupants | int? | Нет | policy | — | Лимит участников комнаты. |
Lobby | bool | Да | false | — | Лобби выключено: допуск решается бэкендом, не лобби-модерацией. |
Алгоритм
- Контекст и доступ. Вызывает prosody
mod_reservations (ExternalSystem). Сначала проверяется подпись:
X-Pin-Signature над сырым телом + свежесть X-Pin-Timestamp (окно ≤ 5 мин); невалидно/просрочено →
MSG_JITSI_SIGNATURE_INVALID. Media-стек активен за CALLS_MEDIA_ENABLED (1:1) / CONFERENCE_CALLS_ENABLED
(конференции); при выключенном флаге допуск не выдаётся.
- Резолв сессии по имени. Из
Name парсятся sessionId + nonce (детерминированная схема Room.RoomName).
Загружается сессия-владелец: 1:1-звонок (call-session.md) или конференция
(conference-session.md). Сессия не найдена или Name не совпадает
точно со стором Room.RoomName → MSG_JITSI_ROOM_UNKNOWN (fail-closed: только backend-провизированные
комнаты имеют право на существование).
- Допустимость состояния. Комната поднимается только для входибельной сессии: конференция в
Provisioning/Active, 1:1-звонок в Ringing/Answered. Терминальные/иные статусы → deny
(MSG_CONFERENCE_INVALID_STATE / MSG_CALL_INVALID_STATE) — prosody отклоняет создание комнаты.
- Решение о записи.
Recording = true, если для сессии есть запись в Pending
(call-recording.md, INV-REC-7) — по ней prosody активирует jibri; иначе
false. Флаг записи решается политикой при старте сессии, здесь только считывается.
- Провизия на allow. Для конференции первое успешное резервирование фиксирует admission-подтверждение
комнаты:
Provisioning → Active (conference-session.md, INV-CONF-4,
StartedAt). Повторный POST на уже Active — идемпотентный no-op с тем же дескриптором. Для 1:1
переход состояния не требуется (комната минтится при → Ringing).
- Результат
POST. Собирается JitsiReservationModel (Id, Name, лимиты, Recording, Lobby=false)
→ 200 OK. Deny → соответствующий 4xx (prosody не создаёт комнату).
DELETE (room destroyed). По roomName резолвится сессия; активный ростер помечается вышедшим
(State = Left, LeaveReason = Disconnected/ConferenceEnded, call-participant.md),
LastActivityAt обновляется. Финальное завершение сессии не форсируется здесь — отдаётся
conference-room-reaper (grace-окно), чтобы не гонять с немедленным реконнектом/пересозданием комнаты.
Неизвестное имя → тихий 200 ack. Всё применяется в одном согласованном изменении.
Диаграмма
Побочные эффекты
| Тип | Детали |
|---|
| Realtime push (hub) | конференция Provisioning → Active — доменный ConferenceStartedEvent уже раздал ConferenceInvite при старте; admission сам инвайтов не шлёт |
| Integration events | none напрямую; завершение сессии (после reaper) публикует pin.messenger.conference-ended.v1 из своего пути |
| External calls | none (это входящий вебхук; допуск/запрет — синхронный ответ prosody) |
| Cache / projections | конференция входит/выходит из partial ix_conf_active; ростер — ix_call_participants_active |
Ошибки и failure modes
| Код | HTTP status | Условие | Сообщение | Retry |
|---|
MSG_JITSI_SIGNATURE_INVALID | 401 | подпись не сошлась / timestamp вне окна replay | «Подпись запроса недействительна» | no retry (переподписать) |
MSG_JITSI_ROOM_UNKNOWN | 403 | Name не резолвится в сессию или не совпадает с Room.RoomName | «Комната неизвестна» | no retry (prosody отклоняет комнату) |
MSG_CONFERENCE_INVALID_STATE | 409 | конференция не в Provisioning/Active (терминал) | «Конференция недоступна» | no retry |
MSG_CALL_INVALID_STATE | 409 | 1:1-звонок не в Ringing/Answered | «Звонок недоступен» | no retry |
VALIDATION_FAILED | 400 | пустой/некорректный Name/MucRoomJid | «Проверьте параметры запроса» | no retry |
DELETE на неизвестное/уже удалённое имя — не ошибка (200 ack): цель уже достигнута.
Acceptance Criteria
- Allow конференции (первое резервирование). Дано конференция в
Provisioning с провизированным
Room.RoomName, валидная подпись, Name совпал точно. Когда POST. Тогда 200 c JitsiReservationModel
(Lobby = false, лимиты, Recording — по наличию Pending-записи), конференция Provisioning → Active
(INV-CONF-4, StartedAt).
- Повторное резервирование. Дано конференция уже
Active. Когда повторный POST. Тогда идемпотентный
no-op, отдаётся тот же дескриптор.
- Allow 1:1-звонка. Дано звонок в
Ringing/Answered. Когда POST. Тогда 200 c дескриптором, без
смены состояния (комната минтится при → Ringing).
- Deny по имени. Дано
Name не резолвится в сессию или не совпал с Room.RoomName. Когда POST. Тогда
MSG_JITSI_ROOM_UNKNOWN (403) — prosody не создаёт комнату (самоподнятых комнат нет, fail-closed).
- Deny по состоянию. Дано конференция в терминальном статусе →
MSG_CONFERENCE_INVALID_STATE (409); 1:1
не в Ringing/Answered → MSG_CALL_INVALID_STATE (409). prosody отклоняет комнату.
- Флаг записи. Дано для сессии есть
Pending-запись (INV-REC-7). Когда allow. Тогда Recording = true
→ prosody активирует jibri; иначе false.
- Уничтожение комнаты. Дано существующая сессия. Когда
DELETE /{roomName}. Тогда активный ростер → Left (LeaveReason), LastActivityAt обновлён, финал сессии отдаётся reaper, 200 ack.
- DELETE неизвестной комнаты. Дано имя уже не соответствует живой комнате. Когда
DELETE. Тогда тихий
200 ack (цель достигнута).
- Неверная подпись. Дано подпись не сошлась / timestamp вне окна. Когда запрос. Тогда
MSG_JITSI_SIGNATURE_INVALID (401).
Совместимость и наблюдаемость
- Новые optional-поля
JitsiReservationModel (MaxOccupants, Duration) — additive; смена схемы имени комнаты
или семантики допуска — breaking (согласованный rollout prosody-модуля). Контракт вебхука версионируется
вместе с media-развёртыванием (ADR-0062).
- Logs:
reservation allow/deny (RoomName, sessionId, reason), room destroyed — без токенов/PII;
metrics: messenger_jitsi_reservation_total{result,reason}, messenger_jitsi_room_destroyed_total; traces —
сквозной traceparent; alert на всплеск deny=room-unknown (рассинхрон имён/попытка самоподнятия комнаты).
Обратные ссылки
Автоссылки: где используется этот документ.
Связанные документы