Интеграция клиента с AeroNyx Chat Relay

AeroNyx21 мин чтения

Справочник по интеграции с AeroNyx Chat Relay: аутентификация WebSocket, запечатанные сообщения 1:1 и групповые сообщения, офлайн-доставка, уведомления о доставке и прочтении, реакции, статус присутствия, зашифрованные вложения, push-уведомления и ограничение частоты запросов.

Подготовить задание для ИИ

Выберите стек и задачу, затем скопируйте задание в помощник по программированию. Оно формируется локально, без отправки в сервис ИИ.

Готовое задание

Это справочник по интеграции с AeroNyx Chat Relay — сервисом WebSocket и HTTPS по адресу api.aeronyx.network, который передаёт между идентичностями AeroNyx сквозно зашифрованные личные сообщения 1:1, групповые сообщения, уведомления, реакции, статус присутствия и зашифрованные вложения.

Документ предназначен для инженеров, разрабатывающих клиенты, ботов и сервисы, совместимые с AeroNyx, а также для ИИ-агентов, реализующих их код. Все кадры, поля и ограничения, описанные на этой странице, соответствуют рабочему ретранслятору и AeroNyx App по состоянию на октябрь 2026 года.

Chat Relay — это централизованный путь доставки. Он сосуществует с децентрализованным путём через узлы (луковичная маршрутизация и анонимные почтовые ящики, см. Проверенная доставка через два узла); клиент может использовать оба пути. Интеграция по HTTPS в формате «запрос — ответ» без WebSocket описана в разделе Central Chat HTTPS API v1.

Модель доверия

Ретранслятор не видит содержимое. Клиенты шифруют и подписывают все данные до того, как они попадают на ретранслятор, а ретранслятор лишь маршрутизирует непрозрачный шифртекст, ставит его в очередь и применяет к нему ограничение частоты запросов.

Ретранслятор никогда не получает:

  • текст сообщений, эмодзи реакций, правки или групповые полезные нагрузки в открытом виде
  • ключи чатов, групповые ключи, ключи вложений или nonce
  • содержимое вложений, имена файлов, миниатюры, звуковые волны или расшифровки
  • закрытые ключи идентичности

При этом ретранслятор видит метаданные доставки, и интеграторам следует считать их доступными оператору:

  • открытые ключи отправителя и получателя, идентификаторы групп и идентификаторы сообщений
  • временные метки, размеры полезной нагрузки, а также состояние доставки, уведомлений и прочтения
  • статус присутствия, состояние активности приложения и индикаторы набора текста (передаются открытыми кадрами)
  • метаданные сигнализации звонков (имя комнаты, идентификатор звонка, признак видео)
  • размер шифртекста вложений, заявленный тип медиа и срок действия
  • флаг contact_request и метаданные соединения на уровне IP

Конфиденциальность содержимого обеспечивается сквозным шифрованием, а не контролем доступа на ретрансляторе. Проектируйте интеграцию с учётом этого: злоумышленник, получивший сохранённый шифртекст, всё равно не должен иметь возможности его прочитать.

Идентичности и ключи

Идентичность чата AeroNyx — это пара ключей Ed25519. Открытый ключ длиной 32 байта, записанный в виде 64 шестнадцатеричных символов в нижнем регистре, служит адресом. Генерируйте ключи на устройстве и никогда никуда не отправляйте закрытый ключ.

Из пары ключей идентичности выводятся два ключа:

  • Ключ чата (1:1). HKDF-SHA256(salt = empty, ikm = X25519(my_secret, peer_public), info = "AERONYX-P2P-KEY", length = 32), где оба ключа Ed25519 преобразуются в X25519 (SHA-512(seed)[0..32] с клампингом для закрытого ключа, преобразование из формы Эдвардса в форму Монтгомери для открытого ключа). Оба участника получают один и тот же ключ.
  • Подписи сообщений. Ed25519 с ключом идентичности над точными байтовыми строками, определёнными на этой странице.

Всегда используйте шестнадцатеричную запись в нижнем регистре для открытых ключей в кадрах. Ретранслятор не нормализует регистр во всех ключах очередей.

Аутентификация

Подпись RelayAuth

Вход по WebSocket и каждая аутентифицируемая конечная точка HTTPS используют одну и ту же подпись:

text
digest    = SHA256("AeroNyx-RelayAuth-v1" || pubkey[32] || timestamp as u64 little-endian)
signature = Ed25519(identity_key, digest)

"AeroNyx-RelayAuth-v1" — это 20 байт ASCII без завершающего символа. timestamp задаётся в секундах Unix и должен отличаться от времени сервера не более чем на 300 секунд.

Подпись связывает только идентичность и время. Она не связывает метод, путь, тело запроса или соединение, и nonce не используется. Генерируйте новую временную метку для каждого запроса, передавайте подпись только по TLS и никогда не записывайте её в журналы.

Заголовок HTTPS

http
Authorization: Relay <pubkey-hex>:<timestamp>:<signature-hex>

При неудачной проверке возвращается HTTP 401 с {"success": false, "error": "<reason>"}. Возможные причины, в частности: missing_auth_header, malformed_auth_header, invalid_timestamp, timestamp_expired, invalid_pubkey и invalid_signature.

Соединение WebSocket

Конечная точка

text
wss://api.aeronyx.network/ws/relay/

Нативные клиенты подключаются без заголовка Origin. Браузеры должны подключаться с разрешённого источника; при любом другом источнике или неизвестном Host соединение закрывается с кодом 1008.

Вход

Сервер принимает сокет, а затем ожидает кадр auth в течение 30 секунд:

json
{
  "type": "auth",
  "pubkey": "<64 hex>",
  "timestamp": 1780000000,
  "signature": "<128 hex>"
}

При успехе:

json
{ "type": "auth_ack", "session_id": "<uuid>", "server_ts": 1780000001 }

Используйте server_ts для оценки расхождения часов; временные метки сообщений проверяются в том же окне ±300 секунд.

После auth_ack сервер запускает механизм пульса, подписывает соединение на свои каналы и сразу же воспроизводит офлайн-очередь (см. Офлайн-доставка).

При неудаче сервер отправляет {"type": "auth_error", "reason": "<reason>"} и закрывает сокет с кодом 4001. Причины: missing_fields, timestamp_expired, invalid_pubkey, invalid_signature_length, invalid_signature_encoding, invalid_signature, internal_error. При истечении времени ожидания входа отправляется причина timeout, и соединение закрывается с кодом 4002.

На любой другой кадр, полученный до входа, сервер отвечает auth_error с причиной authentication_required; сокет при этом остаётся открытым.

Пульс и состояние активности

НаправлениеКадрПоведение
сервер → клиент{"type":"ping"} каждые 30 сОтветить {"type":"pong"}.
клиент → сервер{"type":"ping"}Сервер отвечает {"type":"pong"}.
клиент → сервер{"type":"presence_state","foreground":true}Помечает это соединение как активное.
клиент → сервер{"type":"presence_state","foreground":false}Помечает его как фоновое, что позволяет ретранслятору отправлять push-уведомления о новых сообщениях.

Ретранслятор считает идентичность находящейся в сети только до тех пор, пока её соединение отправляет ping, pong или presence_state с foreground: true не реже одного раза в 90 секунд. AeroNyx App, находясь на переднем плане, отправляет ping каждые 15 секунд и считает соединение разорванным, если пропущено более трёх pong.

Долгоживущие соединения следует переустанавливать не реже одного раза в 24 часа. Сообщения не теряются, если соединение незаметно перестаёт получать кадры в реальном времени, поскольку каждое сообщение также ставится в очередь и воспроизводится при входе; однако доставка в реальном времени возобновляется только после переподключения.

Правила для кадров

  • Только текстовые кадры, по одному объекту JSON на кадр. Двоичные кадры игнорируются.
  • Максимальный размер кадра — 1 048 576 символов. Кадры большего размера отклоняются с {"type":"error","reason":"message_too_large"}. Держите payload_b64 в пределах примерно 800 КиБ, чтобы оставить место для остальной части кадра.
  • Для некорректных кадров возвращается error с причиной invalid_json, invalid_json_type или unknown_type.

Общий кадр ошибки:

json
{ "type": "error", "reason": "<reason>", "retry_after": 0 }

Ошибки ограничения частоты запросов также содержат scope.

Коды закрытия

КодЗначение
1008Host или Origin не разрешены.
4000Серверу не удалось передать сигнал пульса.
4001Ошибка входа.
4002Истекло время ожидания входа.

Переподключайтесь с экспоненциальной задержкой и случайным разбросом. AeroNyx App ожидает 2^(attempt-1) секунд с ограничением диапазоном 1–60 секунд, умножая результат на случайный коэффициент от 0,8 до 1,2.

Отправка сообщения 1:1

1. Сборка запечатанного конверта

Полезная нагрузка сообщения 1:1 — это подписанный зашифрованный ChatEnvelope. Его точная структура, эталонная реализация на Python и эталонный тестовый вектор приведены в разделе Central Chat HTTPS API v1: формат запечатанного конверта. Оба API используют один и тот же конверт.

Вкратце: XChaCha20-Poly1305 с ключом чата, подпись Ed25519 над 121-байтовой стенограммой и фиксированная двоичная структура. content_type равен 0 для всех сообщений, включая сообщения с вложениями.

Открытый текст — это текст в UTF-8 для обычного сообщения или объект JSON для сообщений с вложениями, ответами, пересылками или предпросмотром ссылок:

json
{
  "type": "aeronyx_message",
  "text": "See the attached file",
  "attachments": [ { "blob_id": "...", "file_key": "...", "media_type": "image/jpeg", "file_name": "photo.jpg", "file_size": 482113 } ],
  "reply": { "msg_id": "...", "sender_pubkey": "...", "text": "..." },
  "forwarded": true,
  "forwarded_from_name": "...",
  "forwarded_from_pubkey": "...",
  "link_preview": { "url": "https://...", "title": "...", "description": "...", "site_name": "..." }
}

Все поля, кроме type и text, необязательны. Получатели должны отображать как обычный текст любой открытый текст, который не является объектом JSON с "type": "aeronyx_message". Объект вложения определён в разделе Зашифрованные вложения.

2. Подпись кадра

Каждый кадр сообщения содержит вторую подпись, payload_sig, которую ретранслятор проверяет перед приёмом:

text
payload_sig = Ed25519(identity_key,
    SHA256(receiver_pubkey[32] || sender_pubkey[32] || discriminant as 1 byte
           || timestamp as u64 little-endian || envelope_bytes))

envelope_bytes — это декодированный payload_b64. Используйте дискриминант 11 для сообщений и правок и 12 для реакций. payload_sig кодируется в шестнадцатеричном виде.

3. Отправка

json
{
  "type": "relay_send",
  "msg_id": "9e762ae0f5da43e6bec5eb8143f1f834",
  "receiver_pubkey": "<64 hex>",
  "discriminant": 11,
  "payload_b64": "<base64 envelope>",
  "timestamp": 1780000000,
  "payload_sig": "<128 hex>"
}
ПолеПравила
msg_id32 шестнадцатеричных символа в нижнем регистре: 16-байтовый message_id конверта. Приложение получателя сохраняет сообщение под этим идентификатором, поэтому он должен совпадать с конвертом.
receiver_pubkeyОткрытый ключ получателя.
discriminant11.
timestampСекунды Unix в пределах ±300 с от времени сервера.
suppress_pushНеобязательное. true отключает отправку push-уведомления.
contact_requestНеобязательное. Помечает первое сообщение пользователю, который требует проверки контактов (см. Проверка контактов).

4. Подтверждение

json
{ "type": "relay_delivered", "msg_id": "...", "delivered": true, "queued": true, "durable": true }
  • durable (и queued) принимает значение true, как только сообщение сохранено в офлайн-очереди получателя. Считайте durable: true признаком того, что сообщение «отправлено».
  • delivered означает, что у получателя было активное соединение на переднем плане. Это не доказательство получения; для этого используйте уведомления о доставке.

Ожидайте подтверждения до 15 секунд, прежде чем считать попытку неудачной.

Отклонения и ошибки

json
{ "type": "send_rejected", "msg_id": "...", "reason": "verification_required" }
ОтветПричинаДействие
send_rejectedverification_requiredПолучатель принимает сообщения только от контактов. Не повторять.
send_rejectedcontact_request_rate_limitedДостигнут лимит запросов на добавление в контакты для этого получателя. Не повторять.
errormissing_fieldsОбязательное поле отсутствует или пусто.
errorinvalid_timestamp, timestamp_expiredИсправить часы и заново проставить временную метку.
errorinvalid_payload_sigpayload_sig не проходит проверку.
errorrate_limitedПовторить через retry_after секунд.

Повторные попытки

Повторяйте отправку неподтверждённого сообщения с тем же msg_id и теми же байтами конверта. Поскольку ретранслятор отклоняет кадры старше 300 секунд, для каждой повторной попытки вычисляйте новые timestamp кадра и payload_sig; конверт сохраняет исходную временную метку. Ретранслятор и получатели устраняют дубликаты по msg_id.

Резервный канал HTTPS

Если WebSocket недоступен, то же сообщение можно отправить по HTTPS:

http
POST /api/relay/push/
Authorization: Relay <pubkey>:<timestamp>:<signature>
Content-Type: application/json
json
{
  "receiver_pubkey": "<64 hex>",
  "discriminant": 11,
  "payload_b64": "<base64 envelope>",
  "timestamp": 1780000000,
  "payload_sig": "<128 hex>"
}

При успехе возвращается HTTP 200 с {"success": true}. Ошибки проверки контактов возвращают 400 с verification_required или contact_request_rate_limited, а при ограничении частоты запросов возвращается 429 с Retry-After. Сообщения, отправленные таким способом, ставятся в очередь получателя, но не вызывают push-уведомления, поэтому после восстановления соединения WebSocket отправьте их повторно через него.

Получение сообщений

Входящие сообщения поступают в виде relay_envelope:

json
{
  "type": "relay_envelope",
  "sender_pubkey": "<64 hex>",
  "discriminant": 11,
  "payload_b64": "<base64 envelope>",
  "timestamp": 1780000000,
  "msg_id": "9e762ae0f5da43e6bec5eb8143f1f834",
  "from_offline": false
}

Для discriminant: 11:

  1. Устраните дубликаты по msg_id. Одно и то же сообщение может прийти в реальном времени и повторно из офлайн-очереди.
  2. Разберите конверт и убедитесь, что его receiver_pubkey совпадает с вашей идентичностью.
  3. Для кадров в реальном времени (from_offline: false) отбросьте сообщение, если временная метка конверта отличается от ваших часов более чем на 300 секунд.
  4. Проверьте подпись конверта по sender_pubkey конверта. Аутентифицированным отправителем является именно этот ключ, а не sender_pubkey кадра.
  5. Расшифруйте сообщение ключом чата, выведенным для этого отправителя. Очень старые версии приложения шифровали непосредственно выходом X25519; попробуйте его, если ключ HKDF не подошёл.
  6. Надёжно сохраните сообщение, затем отправьте уведомление о доставке, а для from_offline: true — также офлайн-подтверждение.

Конверты 1:1 доставляются без payload_sig; проверкой подлинности служит подпись конверта. Кадры также могут содержать contact_request: true.

Если сообщение адресовано не вам, не проходит проверку или не расшифровывается, отбросьте его, ничего не показывая пользователю.

Офлайн-доставка

Каждое сообщение, правка, отзыв, реакция, уведомление о доставке и уведомление о прочтении записывается в офлайн-очередь получателя до доставки в реальном времени. Очередь автоматически воспроизводится после каждого входа, а также по запросу:

json
{ "type": "relay_pull" }

Ретранслятор воспроизводит все элементы очереди в виде их обычных типов кадров с from_offline: true, упорядочивая их по временной метке, после чего отправляет:

json
{ "type": "relay_pull_done", "count": 12, "has_more": false }

Доставка выполняется по принципу «как минимум один раз». Элементы остаются в очереди до подтверждения:

json
{ "type": "relay_offline_ack", "msg_id": "9e762ae0f5da43e6bec5eb8143f1f834" }

Реакции подтверждайте с помощью "reaction_id" вместо "msg_id". Подтверждайте элемент только после его надёжного сохранения на устройстве. Ретранслятор отвечает {"type":"relay_offline_ack","msg_id":"...","success":true}.

Ограничения очереди:

ОграничениеЗначение
Элементов на получателя1 000. При заполнении очереди новые элементы отклоняются, а отправитель получает durable: false.
Срок хранения72 часа после добавления самого последнего элемента.
Размер полезной нагрузки1 МиБ после декодирования, в пределах лимита кадра в 1 МиБ.

Ретранслятор — это буфер доставки, а не история сообщений. Храните историю на устройстве.

Уведомления о доставке и прочтении

Уведомление о доставке

Отправляется после сохранения сообщения:

json
{ "type": "message_receipt", "receiver_pubkey": "<original sender>", "msg_id": "...", "timestamp": 1780000010 }

Ретранслятор отвечает message_receipt_ack и доставляет исходному отправителю {"type":"message_receipt","sender_pubkey":"...","msg_id":"...","timestamp":...}.

Уведомление о прочтении

json
{ "type": "message_read", "receiver_pubkey": "<original sender>", "msg_id": "<latest read message>", "timestamp": 1780000200, "enabled": true }

Уведомления о прочтении работают как отметки уровня: приложение помечает прочитанными указанное сообщение и все более ранние исходящие сообщения в беседе. Отправляйте уведомление только в том случае, если у пользователя включены уведомления о прочтении.

Ретранслятор применяет правило взаимности при отправке, при доставке в реальном времени и при воспроизведении очереди. Уведомление о прочтении доставляется только в том случае, если оба пользователя являются взаимными контактами и у обоих включено read_receipts_enabled. В противном случае отправитель получает:

json
{ "type": "message_read_ack", "msg_id": "...", "delivered": false, "suppressed": true, "reason": "receiver_read_receipts_disabled" }
ПричинаЗначение
client_disabledКадр содержал enabled: false или read_receipts_enabled: false.
not_mutual_contactПользователи не являются взаимными контактами.
reader_read_receipts_disabledУ прочитавшего пользователя отключены уведомления о прочтении.
receiver_read_receipts_disabledУ исходного отправителя отключены уведомления о прочтении.
invalid_pubkeyОткрытый ключ имеет некорректный формат.

Доставленное уведомление о прочтении подтверждается кадром {"type":"message_read_ack","msg_id":"...","delivered":true}.

Правки и отзывы

Правка

Правка — это новый запечатанный конверт (со своим случайным message_id), содержащий полное заменяющее содержимое и отправляемый с привязкой к идентификатору исходного сообщения:

json
{
  "type": "message_edit",
  "receiver_pubkey": "<64 hex>",
  "msg_id": "<original msg_id>",
  "target_msg_id": "<original msg_id>",
  "payload_b64": "<base64 envelope>",
  "payload_sig": "<128 hex>",
  "timestamp": 1780000400
}

payload_sig вычисляется по формуле из раздела Подпись кадра с дискриминантом 11. Правка, удаляющая все вложения, устанавливает "attachments_edited": true в своём открытом JSON. Ретранслятор отвечает message_edit_ack. Получатели проверяют и расшифровывают конверт так же, как сообщение, и должны применять правку только в том случае, если её отправитель является автором исходного сообщения.

Отзыв

json
{
  "type": "message_revoke",
  "receiver_pubkey": "<64 hex>",
  "sender_pubkey": "<64 hex>",
  "msg_id": "<original msg_id>",
  "timestamp": 1780000500,
  "payload_sig": "<128 hex>"
}

Подпись отзыва вычисляется над необработанными байтами, без хеширования:

text
payload_sig = Ed25519(identity_key,
    "aeronyx-message-revoke-v1" || sender_pubkey[32] || receiver_pubkey[32]
    || UTF-8(msg_id) || timestamp as u64 little-endian)

Ретранслятор отвечает message_revoke_ack. Ретранслятор не проверяет авторство: получатели должны проверить подпись и применять отзыв только в том случае, если его отправитель является автором исходного сообщения. Чтобы удалить вложения отозванного сообщения, вызовите POST /api/relay/blob/{blob_id}/delete/.

Реакции

Реакция 1:1 — это запечатанный конверт, у которого message_id является идентификатором реакции, с content_type 2 и открытым текстом {"emoji": "❤️", "op": "add"} (или "remove"):

json
{
  "type": "message_reaction",
  "receiver_pubkey": "<64 hex>",
  "msg_id": "<message being reacted to>",
  "reaction_id": "<32 hex>",
  "payload_b64": "<base64 envelope>",
  "payload_sig": "<128 hex>",
  "timestamp": 1780000300
}
  • payload_sig использует дискриминант 12.
  • reaction_id служит ключом идемпотентности. Повторный reaction_id в течение 72 часов подтверждается с "duplicate": true и повторно не доставляется.
  • Ретранслятор отвечает {"type":"message_reaction_ack","msg_id":"...","reaction_id":"...","delivered":...,"queued":...}.
  • Количество реакций ограничено 20 на отправителя и беседу в пределах окна ограничения.

Групповые реакции описаны в разделе Группы.

Присутствие и набор текста

Кадры присутствия и набора текста представляют собой открытые метаданные и видны ретранслятору.

Присутствие

Подписка на контакты:

json
{ "type": "presence_subscribe", "pubkeys": ["<64 hex>", "<64 hex>"] }

В одном кадре учитывается до 200 ключей; некорректные и повторяющиеся ключи игнорируются. Подписки накапливаются в течение всего времени жизни соединения. Подписывайтесь только на собственные контакты.

json
{
  "type": "presence_subscribe_ack",
  "count": 2,
  "updates": [
    { "pubkey": "<a>", "visible": true, "presence_visible": true, "last_seen_visible": true, "online": false, "last_seen_ts": 1780000000, "reason": "allowed" },
    { "pubkey": "<b>", "visible": false, "presence_visible": false, "last_seen_visible": false, "reason": "not_mutual_contact" }
  ],
  "server_ts": 1780000001
}

Статус присутствия виден только между взаимными контактами и только если у целевого пользователя включено presence_enabled. Скрытые записи (reason not_mutual_contact или presence_hidden) не содержат online и last_seen_ts. last_seen_ts присутствует только тогда, когда last_seen_visible равен true; в противном случае показывайте обобщённое состояние, например «был(а) недавно».

Изменения в реальном времени поступают в виде:

json
{ "type": "presence_update", "pubkey": "<a>", "online": true, "presence_visible": true, "last_seen_visible": true, "last_seen_ts": 1780000100 }

Набор текста

json
{ "type": "typing", "receiver_pubkey": "<64 hex>", "is_typing": true }
{ "type": "group_typing", "group_id": "<uuid>", "is_typing": true }

Индикатор набора текста пересылается только в том случае, если статус присутствия отправителя был бы виден получателю (взаимные контакты и presence_enabled), либо другим участникам группы, в которой состоит отправитель. Он никогда не сохраняется, не ставится в очередь и не передаётся через push-уведомления.

Профиль и настройки конфиденциальности

http
GET /api/relay/profile/
Authorization: Relay <pubkey>:<timestamp>:<signature>
json
{
  "success": true,
  "profile": {
    "pubkey_hex": "<64 hex>",
    "display_name": "AeroNyx User",
    "bio": "",
    "handle": "",
    "avatar_url": "",
    "status_text": "",
    "privacy": { "presence_enabled": true, "last_seen_enabled": true, "read_receipts_enabled": true },
    "updated_at": "2026-10-09T10:00:00Z"
  }
}

Идентичность без профиля получает пустые поля и все три флага конфиденциальности со значением true.

http
PATCH /api/relay/profile/
Authorization: Relay <pubkey>:<timestamp>:<signature>
Content-Type: application/json
json
{ "display_name": "Alice", "privacy": { "presence_enabled": true, "last_seen_enabled": false, "read_receipts_enabled": false } }
ПолеПравила
display_nameДо 50 символов.
bioДо 200 символов.
avatar_urlURL https:// или пустое значение.
handlea-z и 0-9, от 5 до 24 символов. На имена пользователей распространяются правила членства и периоды ожидания между изменениями.
privacy.*Логические значения. Три флага также можно передавать на верхнем уровне.

Возможные ошибки, в частности: no_valid_fields, <flag>_invalid_boolean, handle_taken и handle_change_cooldown:<date>.

Поддерживайте поведение клиента в соответствии с этими настройками: не отправляйте уведомления о прочтении, когда они отключены, и не отображайте состояние прочтения собеседника, пока ваши собственные уведомления о прочтении отключены.

Проверка контактов

Пользователь может потребовать, чтобы незнакомцы проходили проверку, прежде чем писать ему. Если получатель включил эту функцию и не добавил отправителя в контакты, relay_send отклоняется с verification_required.

Чтобы начать беседу, отправьте одно сообщение с "contact_request": true. Запросы на добавление в контакты обходят эту проверку и ограничены 3 на пару «отправитель — получатель» в скользящем 24-часовом окне; последующие запросы отклоняются с contact_request_rate_limited. Флаг contact_request доставляется получателю, чтобы приложение могло представить сообщение как запрос.

Проверка контактов применяется к relay_send и к резервному каналу HTTPS.

Группы

Групповые сообщения

Групповое содержимое шифруется общим 32-байтовым групповым ключом с использованием AES-256-GCM:

text
payload_b64 = base64(nonce[12] || AES-256-GCM(group_key, plaintext_json) || tag[16])

Открытый JSON содержит text, type (text, media, system или reaction), sender_pubkey, created_at, а также необязательные attachments, mentions, reply, forwarded, forwarded_from_name, forwarded_from_pubkey и link_preview.

json
{
  "type": "group_send",
  "msg_id": "<32 hex>",
  "group_id": "<uuid>",
  "payload_b64": "<base64>",
  "timestamp": 1780000000,
  "payload_sig": "<128 hex>",
  "key_version": 3
}
text
payload_sig = Ed25519(identity_key,
    SHA256(UTF-8(group_id) || sender_pubkey[32] || 0x19 || timestamp as u64 little-endian
           || decoded payload))

Ретранслятор проверяет подпись и то, что отправитель является активным участником, сохраняет сообщение для каждого из остальных активных участников, а затем доставляет его в реальном времени. Групповые полезные нагрузки не используют конверт 1:1, а групповые конверты доставляются вместе с group_id, key_version и payload_sig, чтобы получатели могли проверить отправителя до расшифровки.

json
{
  "type": "group_delivered",
  "msg_id": "...",
  "group_id": "...",
  "accepted": true,
  "accepted_count": 5,
  "delivered_count": 2,
  "queued_count": 5,
  "failed_count": 0,
  "member_count": 6
}

Отправитель, не являющийся участником группы, получает error с причиной not_a_member.

Групповые правки, отзывы и реакции

КадрПодпись
group_message_edit (group_id, msg_id, target_msg_id, payload_b64, payload_sig, key_version, timestamp)Групповая формула выше.
group_message_reaction (group_id, msg_id, reaction_id, payload_b64, payload_sig, key_version, timestamp)Групповая формула выше. Полезная нагрузка — групповая полезная нагрузка с type: "reaction".
group_message_revoke (group_id, sender_pubkey, msg_id, timestamp, payload_sig)Ed25519 над необработанными байтами `"aeronyx-group-message-revoke-v1"

Каждый из этих кадров подтверждается соответствующим кадром _ack со счётчиками доставки. Получатели применяют правки и отзывы только от исходного автора.

Групповые ключи

Групповые ключи распространяются владельцем группы в виде пакетов ключей для каждого участника: base64(nonce[12] || AES-256-GCM(chat_key(owner, member), group_key) || tag[16]). Конечные точки REST в /api/relay/groups/ позволяют создавать группы, управлять участниками и приглашениями, загружать пакеты ключей, выполнять ротацию ключей (keys/rotate/) и получать текущий пакет вызывающей стороны (keys/me/). Отправители шифруют последней имеющейся у них версией ключа; получатели, у которых отсутствует нужная версия ключа, должны запросить keys/me/ и удерживать сообщение до получения ключа.

Зашифрованные вложения

Вложения шифруются на устройстве, загружаются в виде непрозрачного шифртекста, а ссылки на них передаются внутри зашифрованного сообщения.

Шифрование файла

Для каждого файла сгенерируйте случайный 32-байтовый ключ и случайный 12-байтовый nonce:

text
blob = nonce[12] || AES-256-GCM(file_key, file_bytes) || tag[16]

Загрузите blob. Помещайте file_key и все описательные поля внутрь зашифрованного сообщения и никогда — в запрос на загрузку.

Объект вложения

КлючОбязательныйЗначение
blob_idдаИдентификатор, возвращённый при загрузке.
file_keyда32-байтовый ключ файла в кодировке Base64.
media_typeдаMIME-тип открытого файла.
file_nameдаОтображаемое имя.
file_sizeдаРазмер открытого файла в байтах.
thumb_b64нетМиниатюра JPEG в кодировке Base64, до 64 КиБ.
duration_msнетДлительность аудио или видео.
waveformнетДо 96 чисел в диапазоне [0, 1] для голосовых сообщений.
sticker, sticker_pack, sticker_poseнетИдентификация стикера.
liveнетАнимированная часть Live Photo: {blob_id, file_key, media_type, file_size, duration_ms?, width?, height?}.

Получатели игнорируют вложения без blob_id или file_key. Голосовые сообщения из приложения кодируются в AAC-LC в контейнере MP4 (audio/mp4).

Загрузка: одиночный запрос (до 10 МиБ)

http
POST /api/relay/blob/presign/
Authorization: Relay <pubkey>:<timestamp>:<signature>
Content-Type: application/json
json
{ "file_size": 482141, "media_type": "image/jpeg", "media_kind": "image", "ttl_days": 7 }

file_size — это размер шифртекста. media_kind принимает одно из значений voice, image, video, file, avatar, other. ttl_days ограничивается диапазоном 1–30 (по умолчанию 7).

json
{
  "blob_id": "<uuid>",
  "upload_url": "https://...",
  "upload_method": "PUT",
  "storage": "r2",
  "expires_at": "2026-10-16T10:00:00Z",
  "media_type": "image/jpeg",
  "media_kind": "image",
  "access_mode": "capability",
  "ttl_days": 7,
  "max_bytes": 10485760
}

Затем:

  1. Отправьте шифртекст методом PUT на upload_url в течение 15 минут, указав только заголовок Content-Type. Не передавайте заголовок Authorization в хранилище.
  2. Вызовите POST /api/relay/blob/{blob_id}/complete/ с RelayAuth. Ретранслятор подтверждает объект и возвращает {blob_id, file_size, expires_at, storage}.

Загрузка: multipart (до 100 МиБ)

http
POST /api/relay/blob/multipart/create/
json
{ "total_size": 52428800, "media_type": "video/mp4", "media_kind": "video", "ttl_days": 7 }
json
{
  "blob_id": "<uuid>",
  "upload_id": "...",
  "part_size": 8388608,
  "total_parts": 7,
  "part_urls": ["https://...", "..."],
  "storage": "r2",
  "expires_at": "...",
  "max_bytes": 104857600
}

part_size по умолчанию равен 8 МиБ; можно запросить значение от 5 до 16 МиБ. URL частей действительны в течение 60 минут. Отправьте каждую часть методом PUT на её URL и сохраните заголовок ответа ETag, затем:

http
POST /api/relay/blob/multipart/complete/
json
{ "blob_id": "<uuid>", "upload_id": "...", "parts": [ { "part_number": 1, "etag": "\"...\"" } ] }

AeroNyx App использует загрузку одиночным запросом для шифртекста размером до 8 МиБ и multipart-загрузку для файлов большего размера.

Скачивание

http
GET /api/relay/blob/{blob_id}/

Ретранслятор отвечает 302 с заголовком Location, указывающим на сеть доставки контента, а также с заголовками X-AeroNyx-Blob-Size, X-AeroNyx-Blob-Media-Type и X-AeroNyx-Blob-Storage. Выполните переход по перенаправлению самостоятельно, не передавая заголовок Authorization, затем проверьте и расшифруйте blob с помощью его file_key. Ограничьте объём скачивания ожидаемым размером.

blob_id — это мандат на предъявителя: любой, кто им владеет, может получить шифртекст, поэтому ключ передаётся только внутри зашифрованного сообщения. access_mode и срок действия применяются при перенаправлении на ретрансляторе. Не полагайтесь на них для обеспечения конфиденциальности.

Удаление вложений

http
POST /api/relay/blob/{blob_id}/delete/

Удалить blob может только тот, кто его загрузил. Ответ: {"blob_id": "...", "deleted": true}.

Ошибки вложений

Ошибки возвращаются в формате {"success": false, "error": "<text>", "error_code": "<code>"}. Ориентируйтесь на error_code.

HTTPerror_codeЗначение
400blob_id_invalidНекорректный идентификатор blob.
400blob_missing_file_size, blob_missing_total_size, blob_missing_parts, blob_bad_parts, blob_bad_part_countНедопустимый запрос на загрузку.
400blob_not_r2, blob_multipart_complete_failedНе удалось завершить загрузку; начните новую.
401auth_requiredДля blob требуется RelayAuth.
403blob_not_uploader, download_forbiddenВызывающей стороне доступ запрещён.
404blob_not_found, blob_not_uploadedНеизвестный blob или попытка завершения до окончания загрузки.
410blob_expiredСрок действия blob истёк. Попросите отправителя отправить файл повторно.
413blob_too_largeПревышен лимит; ответ содержит max_bytes и chunked_max_bytes.
503blob_r2_unavailableХранилище временно недоступно; повторите попытку с задержкой.

Устаревшие конечные точки загрузки

POST /api/relay/blob/ (multipart-форма, до 10 МиБ) и API возобновляемых сессий в /api/relay/blob/session/ (до 100 МиБ, фрагменты от 64 КиБ до 4 МиБ, сессии действительны 24 часа) остаются доступными в качестве резервного варианта. Новые клиенты должны использовать описанные выше конечные точки.

Push-уведомления

Ретранслятор отправляет уведомления через Apple Push Notification service (APNs) для iOS и macOS. Клиенты Android получают сообщения только через WebSocket.

Push-уведомление отправляется для relay_send и group_send, если у получателя нет активного соединения на переднем плане, отправитель не установил suppress_push, а получатель не отключил уведомления для беседы. Правки, отзывы, реакции и уведомления о доставке и прочтении push-уведомлений не вызывают. Полезные нагрузки push-уведомлений не содержат шифртекста:

json
{
  "aps": { "alert": { "title": "AeroNyx", "body": "New encrypted message" }, "sound": "default", "badge": 1, "content-available": 1 },
  "kind": "p2p_message",
  "sender_pubkey": "<64 hex>",
  "message_id": "..."
}

kind принимает значение p2p_message (вместе с sender_pubkey) или group_message (вместе с group_id). Уведомления о звонках используют missed_call и специальные полезные нагрузки для звонков. Получив push-уведомление, подключитесь и выполните relay_pull.

Конечная точкаТело запроса
POST /api/relay/push/register/token (64 hex), platform (ios или macos), bundle_id, environment (production или sandbox), необязательные token_type (alert или voip) и provider (apns).
POST /api/relay/push/unregister/token, необязательные platform, token_type, provider.
POST /api/relay/push/mute/kind (p2p или group), target (открытый ключ или идентификатор группы), muted (логическое значение).

Все три требуют RelayAuth. При регистрации токен закрепляется за вызывающей идентичностью.

Звонки

Сигнализация голосовых и видеозвонков передаётся через тот же WebSocket в виде открытых метаданных; медиапотоки передаются отдельно. Для звонков 1:1 используются кадры call_invite, call_answer, call_reject, call_hangup и call_busy, для групп — group_call_invite, group_call_invite_broadcast, group_call_answer и group_call_hangup_broadcast, а для встреч с организатором — кадры допуска участников.

Ретранслятор проверяет room_name по участникам: p2p_, за которым следуют первые 16 шестнадцатеричных символов SHA256(lower_key + ":" + higher_key), для звонков 1:1 и grp_<first 8 characters of group_id>_<first 8 hex of SHA256(group_id)> для групп. Сигналы звонков для получателя, находящегося не в сети, хранятся 120 секунд.

Ограничение частоты запросов

ОбластьЛимитОтвет
relay_send, group_send и push по HTTPS, на идентичность50 за короткое окно; 100 000 в суткиWebSocket: error rate_limited с retry_after. HTTPS: 429 с Retry-After.
group_send на отправителя и группу10 за короткое окноerror rate_limited, scope: "sender".
group_send на группу50 за короткое окноerror rate_limited, scope: "group".
Реакции на отправителя и беседу20 за короткое окноerror rate_limited, scope: "reaction".
Запросы на добавление в контакты на пару «отправитель — получатель»3 за 24 часаsend_rejected contact_request_rate_limited.

Выжидайте не менее retry_after секунд. Не повторяйте отправку в плотном цикле: счётчики продолжают расти во время повторных попыток.

Контрольный список реализации

  1. Сгенерируйте и сохраните идентичность Ed25519; везде используйте ключи в шестнадцатеричной записи в нижнем регистре.
  2. Реализуйте RelayAuth, а также вход по WebSocket, пульс и presence_state.
  3. Реализуйте запечатанный конверт и проверьте его по эталонному вектору из Central Chat HTTPS API v1.
  4. Отправляйте через relay_send, считайте durable: true признаком отправки, при повторных попытках заново вычисляйте timestamp и payload_sig.
  5. Принимайте relay_envelope: устраняйте дубликаты, проверяйте, расшифровывайте, сохраняйте, затем отправляйте message_receipt и relay_offline_ack.
  6. Выполняйте relay_pull после входа и при пробуждении по push-уведомлению; подтверждайте только после надёжного сохранения.
  7. Считывайте флаги конфиденциальности профиля и соблюдайте их для статуса присутствия и уведомлений о прочтении.
  8. Шифруйте вложения на устройстве и загружайте их через presign или multipart; храните file_key внутри зашифрованного сообщения.
  9. Проверяйте авторство перед применением правок и отзывов.
  10. Храните поиск и историю сообщений на устройстве. Ретранслятор не поддерживает поиск по содержимому и не является архивом.
<!-- faq:start -->

Часто задаваемые вопросы

Может ли ретранслятор чата AeroNyx читать мои сообщения?

Нет. Сообщения, правки, реакции, групповые сообщения и вложения шифруются и подписываются на устройстве отправителя до того, как попадают на ретранслятор, а ключи никогда не покидают устройства участников разговора. Ретранслятор хранит и пересылает только шифртекст.

Что видит ретранслятор чата AeroNyx?

Ретранслятор видит метаданные доставки: открытые ключи отправителя и получателя, идентификаторы групп и сообщений, метки времени, размеры полезной нагрузки, статус доставки и прочтения, сигналы присутствия и набора текста, метаданные сигнализации звонков, размеры вложений и IP-адреса подключений. Он не видит ни содержимого сообщений, ни содержимого вложений, ни ключей. Полный список приведён в модели доверия в начале этой страницы.

Какое шифрование использует чат AeroNyx?

Каждая идентичность — это пара ключей Ed25519. Два собеседника выводят общий ключ чата с помощью X25519 и HKDF-SHA256. Сообщения 1:1 шифруются с помощью XChaCha20-Poly1305 и подписываются с помощью Ed25519. Групповые сообщения и вложения шифруются с помощью AES-256-GCM. Точные байтовые форматы и тестовый вектор опубликованы в документации HTTPS API центрального чата v1.

Как защищены фото, видео и файлы?

Перед загрузкой каждый файл шифруется на устройстве собственным случайным ключом AES-256-GCM. Хранилище получает только шифртекст. Ключ файла передаётся внутри сквозно зашифрованного сообщения, поэтому расшифровать файл могут только получатели.

Что происходит, если получатель не в сети?

Ретранслятор хранит зашифрованные сообщения в офлайн-очереди получателя до 72 часов и доставляет их, когда получатель снова подключается. На iOS и macOS получатель также получает push-уведомление, не содержащее текста сообщения.

Хранит ли AeroNyx историю моих чатов?

Нет. Ретранслятор — это буфер доставки: элементы удаляются после того, как устройство получателя подтвердит их сохранение. История чатов и поиск находятся на ваших устройствах.

Могу ли я создать собственный клиент или бот для AeroNyx?

Да. Любое программное обеспечение, которое владеет идентичностью Ed25519 и реализует форматы, описанные на этой странице, может обмениваться сообщениями с пользователями AeroNyx App. Для более простой интеграции по схеме «запрос — ответ» без WebSocket используйте HTTPS API центрального чата v1.

Является ли ретранслятор чата AeroNyx децентрализованным?

Ретранслятор чата — это централизованный сервис доставки AeroNyx. Кроме того, AeroNyx управляет сетью узлов с открытым исходным кодом (AGPL-3.0), которая может передавать шифртекст чата по двухскачковому маршруту через разные сети. Оба пути сосуществуют: ретранслятор обеспечивает быструю и надёжную доставку и офлайн-очереди, а путь через узлы — это необязательный маршрут, который сокращает объём данных, доступных для наблюдения любому отдельному оператору.

Почему моё сообщение отклоняется с ошибкой verification_required?

Получатель принимает сообщения только от контактов. Отправьте одно первое сообщение с contact_request: true — получатель увидит его как запрос на добавление в контакты. Допускается не более трёх запросов на добавление в контакты на одного получателя в любом 24-часовом окне.

<!-- faq:end --> <!-- verified-two-hop-delivery-v1:start -->

Проверенная доставка через два узла

Для допустимого аутентифицированного трафика ChatRelay источник может выбрать двухузловой путь с сетевым разнесением и засчитывать доставку только после проверки подписанного подтверждения от ожидаемого конечного узла. Узлы-ретрансляторы маршрутизируют шифртекст и не разбирают сквозно зашифрованную полезную нагрузку. Полная модель доказательств описана в разделе Обнаружение узлов и проверенная зашифрованная доставка через ретрансляторы.

<!-- verified-two-hop-delivery-v1:end -->