Integração de clientes com o AeroNyx Chat Relay

AeroNyx24 min de leitura

Referência de integração do AeroNyx Chat Relay: autenticação WebSocket, mensagens seladas 1:1 e de grupo, entrega offline, confirmações, reações, status de presença, anexos criptografados, notificações push e limites de taxa.

Gerar instruções de integração

Escolha a tecnologia e a tarefa e copie as instruções para seu assistente de programação. A geração é local, sem envio a serviços de IA.

Instruções geradas

Esta é a referência de integração do AeroNyx Chat Relay: o serviço WebSocket e HTTPS em api.aeronyx.network que transporta mensagens 1:1 com criptografia de ponta a ponta, mensagens de grupo, confirmações, reações, status de presença e anexos criptografados entre identidades AeroNyx.

Ela é destinada a engenheiros que desenvolvem clientes, bots e serviços compatíveis com a AeroNyx, e a agentes de programação com IA que os implementam. Cada quadro, campo e limite desta página reflete o retransmissor de produção e o AeroNyx App em outubro de 2026.

O Chat Relay é o caminho de entrega centralizado. Ele coexiste com o caminho descentralizado por nós (roteamento cebola e caixas postais anônimas; consulte Entrega verificada em dois saltos); um cliente pode usar ambos. Para uma integração HTTPS de requisição/resposta sem WebSocket, consulte Central Chat HTTPS API v1.

Modelo de confiança

O retransmissor não tem visibilidade do conteúdo. Os clientes criptografam e assinam tudo antes que chegue ao retransmissor, e o retransmissor roteia, enfileira e aplica limites de taxa a texto cifrado opaco.

O retransmissor nunca recebe:

  • texto de mensagens, emojis de reações, edições ou payloads de grupo em texto claro
  • chaves de chat, chaves de grupo, chaves de anexos ou nonces
  • conteúdo de anexos, nomes de arquivo, miniaturas, formas de onda ou transcrições
  • chaves privadas de identidade

O retransmissor observa, sim, metadados de entrega, e os integradores devem tratá-los como visíveis para o operador:

  • chaves públicas do remetente e do destinatário, IDs de grupo e IDs de mensagem
  • carimbos de data/hora, tamanhos de payload e estado de entrega, confirmação e leitura
  • status de presença, estado de primeiro plano e indicadores de digitação (enviados como quadros em texto claro)
  • metadados de sinalização de chamadas (nome da sala, ID da chamada, indicador de vídeo)
  • tamanho do texto cifrado dos anexos, tipo de mídia declarado e expiração
  • o indicador contact_request e os metadados de conexão em nível de IP

A confidencialidade do conteúdo vem da criptografia de ponta a ponta, não do controle de acesso no retransmissor. Projete de acordo: um invasor que obtenha um texto cifrado armazenado ainda deve ser incapaz de lê-lo.

Identidades e chaves

Uma identidade de chat AeroNyx é um par de chaves Ed25519. A chave pública de 32 bytes, escrita como 64 caracteres hexadecimais minúsculos, é o endereço. Gere as chaves no dispositivo e nunca envie a chave privada para lugar algum.

Duas chaves são derivadas de um par de identidade:

  • Chave de chat (1:1). HKDF-SHA256(salt = empty, ikm = X25519(my_secret, peer_public), info = "AERONYX-P2P-KEY", length = 32), em que ambas as chaves Ed25519 são convertidas para X25519 (SHA-512(seed)[0..32] com clamping para a chave secreta, conversão de Edwards para Montgomery para a chave pública). Os dois pares derivam a mesma chave.
  • Assinaturas de mensagens. Ed25519 com a chave de identidade, sobre as sequências exatas de bytes definidas nesta página.

Use sempre hexadecimal minúsculo para chaves públicas nos quadros. O retransmissor não normaliza maiúsculas e minúsculas em todas as chaves de fila.

Autenticação

Assinatura RelayAuth

O login por WebSocket e todos os endpoints HTTPS autenticados usam a mesma assinatura:

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

"AeroNyx-RelayAuth-v1" são os 20 bytes ASCII sem terminador. timestamp é expresso em segundos Unix e deve estar a menos de 300 segundos do horário do servidor.

A assinatura vincula apenas a identidade e o horário. Ela não vincula o método, o caminho, o corpo nem a conexão, e não há nonce. Gere um carimbo de data/hora novo para cada requisição, envie-o somente por TLS e nunca o registre em logs.

Cabeçalho HTTPS

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

Uma verificação malsucedida retorna HTTP 401 com {"success": false, "error": "<reason>"}. Os motivos incluem missing_auth_header, malformed_auth_header, invalid_timestamp, timestamp_expired, invalid_pubkey e invalid_signature.

Conexão WebSocket

Endpoint

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

Clientes nativos se conectam sem cabeçalho Origin. Navegadores devem se conectar a partir de uma origem permitida; qualquer outra origem, ou um Host desconhecido, é fechada com o código 1008.

Login

O servidor aceita o socket e, em seguida, espera um quadro auth em até 30 segundos:

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

Sucesso:

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

Use server_ts para estimar a diferença de relógio; os carimbos de data/hora das mensagens são verificados na mesma janela de ±300 segundos.

Após auth_ack, o servidor inicia sua pulsação, inscreve a conexão em seus canais e reproduz imediatamente a fila offline (consulte Entrega offline).

Em caso de falha, o servidor envia {"type": "auth_error", "reason": "<reason>"} e fecha o socket com o código 4001. Motivos: missing_fields, timestamp_expired, invalid_pubkey, invalid_signature_length, invalid_signature_encoding, invalid_signature, internal_error. O tempo esgotado de login envia o motivo timeout e fecha com 4002.

Qualquer outro quadro antes do login é respondido com auth_error com o motivo authentication_required; o socket permanece aberto.

Pulsação e estado de primeiro plano

DireçãoQuadroComportamento
servidor → cliente{"type":"ping"} a cada 30 sResponda {"type":"pong"}.
cliente → servidor{"type":"ping"}O servidor responde {"type":"pong"}.
cliente → servidor{"type":"presence_state","foreground":true}Marca esta conexão como ativa.
cliente → servidor{"type":"presence_state","foreground":false}Marca a conexão como em segundo plano, para que o retransmissor possa enviar uma notificação push para novas mensagens.

O retransmissor considera uma identidade online somente enquanto sua conexão enviar ping, pong ou presence_state com foreground: true pelo menos a cada 90 segundos. O AeroNyx App envia ping a cada 15 segundos enquanto está em primeiro plano e trata mais de três pongs perdidos como conexão inativa.

Conexões de longa duração devem se reconectar pelo menos uma vez a cada 24 horas. Mensagens nunca são perdidas quando uma conexão deixa de receber quadros ao vivo sem aviso, porque toda mensagem também é enfileirada e reproduzida no login, mas a entrega ao vivo só é retomada após a reconexão.

Regras de quadros

  • Somente quadros de texto, um objeto JSON por quadro. Quadros binários são ignorados.
  • O tamanho máximo do quadro é de 1.048.576 caracteres. Quadros maiores são recusados com {"type":"error","reason":"message_too_large"}. Mantenha payload_b64 abaixo de cerca de 800 KiB para deixar espaço para o restante do quadro.
  • Quadros malformados retornam error com o motivo invalid_json, invalid_json_type ou unknown_type.

Quadro de erro genérico:

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

Erros de limite de taxa também incluem scope.

Códigos de fechamento

CódigoSignificado
1008Host ou Origin não permitido.
4000O servidor não conseguiu enviar sua pulsação.
4001O login falhou.
4002O tempo de login se esgotou.

Reconecte com backoff exponencial e jitter. O AeroNyx App aguarda 2^(attempt-1) segundos, limitados ao intervalo de 1 a 60 segundos, multiplicados por um fator aleatório entre 0,8 e 1,2.

Envio de uma mensagem 1:1

1. Construir o envelope selado

O payload de uma mensagem 1:1 é um ChatEnvelope assinado e criptografado. Sua construção exata, uma implementação de referência em Python e um vetor de teste de referência estão em Central Chat HTTPS API v1: formato do envelope selado. O mesmo envelope é usado nas duas APIs.

Em resumo: XChaCha20-Poly1305 com a chave de chat, uma assinatura Ed25519 sobre uma transcrição de 121 bytes e um layout binário fixo. content_type é 0 para todas as mensagens, inclusive mensagens com anexos.

O texto claro é o texto UTF-8 em uma mensagem simples, ou um objeto JSON em mensagens com anexos, respostas, encaminhamentos ou pré-visualizações de links:

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": "..." }
}

Todos os campos, exceto type e text, são opcionais. Os destinatários devem exibir como texto simples qualquer texto claro que não seja um objeto JSON com "type": "aeronyx_message". O objeto de anexo é definido em Anexos criptografados.

2. Assinar o quadro

Todo quadro de mensagem carrega uma segunda assinatura, payload_sig, que o retransmissor verifica antes de aceitá-lo:

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 é o payload_b64 decodificado. Use o discriminante 11 para mensagens e edições e 12 para reações. payload_sig é codificado em hexadecimal.

3. Enviar

json
{
  "type": "relay_send",
  "msg_id": "9e762ae0f5da43e6bec5eb8143f1f834",
  "receiver_pubkey": "<64 hex>",
  "discriminant": 11,
  "payload_b64": "<base64 envelope>",
  "timestamp": 1780000000,
  "payload_sig": "<128 hex>"
}
CampoRegras
msg_id32 caracteres hexadecimais minúsculos: o message_id de 16 bytes do envelope. O App destinatário armazena a mensagem sob esse ID, portanto ele deve corresponder ao envelope.
receiver_pubkeyA chave pública do destinatário.
discriminant11.
timestampSegundos Unix, dentro de ±300 s do horário do servidor.
suppress_pushOpcional. true não envia nenhuma notificação push.
contact_requestOpcional. Marca uma primeira mensagem para alguém que exige verificação de contato (consulte Verificação de contato).

4. Confirmação de recebimento

json
{ "type": "relay_delivered", "msg_id": "...", "delivered": true, "queued": true, "durable": true }
  • durable (e queued) é true assim que a mensagem é armazenada na fila offline do destinatário. Trate durable: true como "enviada".
  • delivered significa que o destinatário tinha uma conexão ativa em primeiro plano. Não é prova de recebimento; para isso, use as confirmações de entrega.

Aguarde até 15 segundos pela confirmação antes de considerar a tentativa como falha.

Rejeições e erros

json
{ "type": "send_rejected", "msg_id": "...", "reason": "verification_required" }
RespostaMotivoAção
send_rejectedverification_requiredO destinatário aceita mensagens apenas de contatos. Não tente novamente.
send_rejectedcontact_request_rate_limitedLimite de solicitações de contato atingido para este destinatário. Não tente novamente.
errormissing_fieldsUm campo obrigatório está ausente ou vazio.
errorinvalid_timestamp, timestamp_expiredCorrija o relógio e gere um novo carimbo de data/hora.
errorinvalid_payload_sigpayload_sig não passa na verificação.
errorrate_limitedTente novamente após retry_after segundos.

Novas tentativas

Reenvie uma mensagem sem confirmação com o mesmo msg_id e os mesmos bytes de envelope. Como o retransmissor rejeita quadros com mais de 300 segundos, calcule um novo timestamp de quadro e um novo payload_sig a cada nova tentativa; o envelope mantém seu carimbo de data/hora original. O retransmissor e os destinatários eliminam duplicatas por msg_id.

Alternativa via HTTPS

Quando o WebSocket não está disponível, a mesma mensagem pode ser enviada por 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>"
}

O sucesso é HTTP 200 com {"success": true}. Falhas de verificação de contato retornam 400 com verification_required ou contact_request_rate_limited, e o limite de taxa retorna 429 com Retry-After. Mensagens enviadas dessa forma são enfileiradas para o destinatário, mas não disparam notificação push; portanto, reenvie pelo WebSocket assim que ele se reconectar.

Recebimento de mensagens

As mensagens recebidas chegam como relay_envelope:

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

Para discriminant: 11:

  1. Elimine duplicatas por msg_id. A mesma mensagem pode chegar ao vivo e novamente pela fila offline.
  2. Analise o envelope e verifique se o receiver_pubkey dele é a sua identidade.
  3. Para quadros ao vivo (from_offline: false), descarte a mensagem se o carimbo de data/hora do envelope diferir em mais de 300 segundos do seu relógio.
  4. Verifique a assinatura do envelope com o sender_pubkey do envelope. Essa chave, e não o sender_pubkey do quadro, é o remetente autenticado.
  5. Descriptografe com a chave de chat derivada desse remetente. Versões muito antigas do App criptografavam com a saída X25519 bruta; tente-a se a chave HKDF falhar.
  6. Armazene a mensagem de forma persistente e, em seguida, envie uma confirmação de entrega e, para from_offline: true, uma confirmação da fila offline.

Envelopes 1:1 são entregues sem payload_sig; a assinatura do envelope é a verificação de autenticidade. Os quadros também podem conter contact_request: true.

Se uma mensagem não for endereçada a você, falhar na verificação ou na descriptografia, descarte-a sem exibir nada.

Entrega offline

Toda mensagem, edição, revogação, reação, confirmação de entrega e confirmação de leitura é gravada na fila offline do destinatário antes da entrega ao vivo. A fila é reproduzida automaticamente após cada login e sob demanda:

json
{ "type": "relay_pull" }

O retransmissor reproduz todos os itens enfileirados com seus tipos de quadro normais e from_offline: true, ordenados por carimbo de data/hora, seguidos de:

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

A entrega é do tipo "pelo menos uma vez". Os itens permanecem na fila até serem confirmados:

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

Confirme reações com "reaction_id" em vez de "msg_id". Confirme somente depois que o item estiver armazenado de forma persistente no dispositivo. O retransmissor responde com {"type":"relay_offline_ack","msg_id":"...","success":true}.

Limites da fila:

LimiteValor
Itens por destinatário1.000. Quando a fila está cheia, novos itens são rejeitados e o remetente recebe durable: false.
Retenção72 horas após a inclusão do item mais recente.
Tamanho do payload1 MiB decodificado, dentro do limite de quadro de 1 MiB.

O retransmissor é um buffer de entrega, não um histórico de mensagens. Mantenha o histórico no dispositivo.

Confirmações de entrega e de leitura

Confirmação de entrega

Envie após armazenar uma mensagem:

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

O retransmissor responde message_receipt_ack e entrega {"type":"message_receipt","sender_pubkey":"...","msg_id":"...","timestamp":...} ao remetente original.

Confirmação de leitura

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

Confirmações de leitura funcionam como marcas d'água: o App marca como lidas a mensagem indicada e todas as mensagens enviadas anteriores da conversa. Envie uma somente quando o usuário tiver as confirmações de leitura ativadas.

O retransmissor aplica uma regra de reciprocidade no envio, na entrega ao vivo e na reprodução. Uma confirmação de leitura só é entregue se ambos os usuários forem contatos mútuos e ambos tiverem read_receipts_enabled. Caso contrário, o remetente recebe:

json
{ "type": "message_read_ack", "msg_id": "...", "delivered": false, "suppressed": true, "reason": "receiver_read_receipts_disabled" }
MotivoSignificado
client_disabledO quadro continha enabled: false ou read_receipts_enabled: false.
not_mutual_contactOs usuários não são contatos mútuos.
reader_read_receipts_disabledO leitor desativou as confirmações de leitura.
receiver_read_receipts_disabledO remetente original desativou as confirmações de leitura.
invalid_pubkeyUma chave pública está malformada.

Uma confirmação de leitura entregue é confirmada com {"type":"message_read_ack","msg_id":"...","delivered":true}.

Edições e revogações

Edição

Uma edição é um novo envelope selado (com seu próprio message_id aleatório) que contém o conteúdo substituto completo, enviado com referência ao ID da mensagem original:

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 usa a fórmula de Assinar o quadro com o discriminante 11. Uma edição que remove todos os anexos define "attachments_edited": true em seu JSON em texto claro. O retransmissor responde message_edit_ack. Os destinatários verificam e descriptografam o envelope como uma mensagem e só devem aplicar uma edição se o remetente dela tiver escrito a mensagem original.

Revogação

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

A assinatura de revogação é calculada sobre os bytes brutos, sem hash:

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)

O retransmissor responde message_revoke_ack. O retransmissor não verifica a autoria: os destinatários devem verificar a assinatura e aplicar uma revogação somente se o remetente dela tiver escrito a mensagem original. Para excluir os anexos de uma mensagem revogada, chame POST /api/relay/blob/{blob_id}/delete/.

Reações

Uma reação 1:1 é um envelope selado cujo message_id é o ID da reação, com content_type 2 e o texto claro {"emoji": "❤️", "op": "add"} (ou "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 usa o discriminante 12.
  • reaction_id é a chave de idempotência. Um reaction_id repetido em até 72 horas é confirmado com "duplicate": true e não é entregue novamente.
  • O retransmissor responde {"type":"message_reaction_ack","msg_id":"...","reaction_id":"...","delivered":...,"queued":...}.
  • As reações são limitadas a 20 por remetente e conversa dentro da janela de limite de taxa.

As reações em grupo são descritas em Grupos.

Status de presença e digitação

Os quadros de status de presença e de digitação são metadados em texto claro e são visíveis para o retransmissor.

Status de presença

Inscreva-se nos contatos:

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

São consideradas até 200 chaves por quadro; chaves malformadas e duplicadas são ignoradas. As inscrições se acumulam durante toda a vida útil da conexão. Inscreva-se apenas nos seus próprios contatos.

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
}

O status de presença só é visível entre contatos mútuos, e somente se o alvo tiver presence_enabled. Entradas ocultas (reason not_mutual_contact ou presence_hidden) não contêm online nem last_seen_ts. last_seen_ts só está presente quando last_seen_visible é true; caso contrário, exiba um estado genérico, como "visto recentemente".

As alterações ao vivo chegam como:

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

Digitação

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

O indicador de digitação só é encaminhado quando o status de presença do remetente seria visível para o destinatário (contatos mútuos e presence_enabled), ou para os demais membros de um grupo ao qual o remetente pertence. Ele nunca é armazenado, enfileirado ou enviado por push.

Perfil e configurações de privacidade

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"
  }
}

Uma identidade sem perfil recebe campos vazios e os três indicadores de privacidade como 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 } }
CampoRegras
display_nameAté 50 caracteres.
bioAté 200 caracteres.
avatar_urlURL https:// ou vazio.
handlea-z e 0-9, de 5 a 24 caracteres. Os nomes de usuário estão sujeitos às regras de assinatura e a períodos de espera entre alterações.
privacy.*Booleanos. Os três indicadores também podem ser enviados no nível superior.

Os erros incluem no_valid_fields, <flag>_invalid_boolean, handle_taken e handle_change_cooldown:<date>.

Mantenha o cliente consistente com essas configurações: não envie confirmações de leitura quando estiverem desativadas e não exiba o estado de leitura de um contato enquanto suas próprias confirmações de leitura estiverem desativadas.

Verificação de contato

Um usuário pode exigir que desconhecidos sejam verificados antes de enviar mensagens. Quando o destinatário ativou essa opção e não adicionou o remetente como contato, relay_send é rejeitado com verification_required.

Para iniciar uma conversa, envie uma mensagem com "contact_request": true. Solicitações de contato ignoram a verificação e são limitadas a 3 por remetente e destinatário em uma janela móvel de 24 horas; solicitações adicionais são rejeitadas com contact_request_rate_limited. O indicador contact_request é entregue ao destinatário para que o App possa apresentar a mensagem como uma solicitação.

A verificação de contato se aplica a relay_send e à alternativa via HTTPS.

Grupos

Mensagens de grupo

O conteúdo de grupo é criptografado com uma chave de grupo compartilhada de 32 bytes usando AES-256-GCM:

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

O JSON em texto claro contém text, type (text, media, system ou reaction), sender_pubkey, created_at e, opcionalmente, attachments, mentions, reply, forwarded, forwarded_from_name, forwarded_from_pubkey e 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))

O retransmissor verifica a assinatura e se o remetente é um membro ativo, armazena a mensagem para cada um dos demais membros ativos e, em seguida, a entrega ao vivo. Payloads de grupo não usam o envelope 1:1, e os envelopes de grupo são entregues com group_id, key_version e payload_sig para que os destinatários possam verificar o remetente antes de descriptografar.

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
}

Um remetente que não é membro recebe error com o motivo not_a_member.

Edições, revogações e reações em grupo

QuadroAssinatura
group_message_edit (group_id, msg_id, target_msg_id, payload_b64, payload_sig, key_version, timestamp)Fórmula de grupo acima.
group_message_reaction (group_id, msg_id, reaction_id, payload_b64, payload_sig, key_version, timestamp)Fórmula de grupo acima. O payload é um payload de grupo com type: "reaction".
group_message_revoke (group_id, sender_pubkey, msg_id, timestamp, payload_sig)Ed25519 bruto sobre `"aeronyx-group-message-revoke-v1"

Cada um é confirmado com o quadro _ack correspondente, que contém as contagens de entrega. Os destinatários aplicam edições e revogações somente do autor original.

Chaves de grupo

As chaves de grupo são distribuídas pelo proprietário do grupo como pacotes de chaves por membro: base64(nonce[12] || AES-256-GCM(chat_key(owner, member), group_key) || tag[16]). Os endpoints REST em /api/relay/groups/ criam grupos, gerenciam membros e convites, enviam pacotes de chaves, rotacionam chaves (keys/rotate/) e obtêm o pacote atual do chamador (keys/me/). Os remetentes criptografam com a versão de chave mais recente que possuem; destinatários que não têm uma versão de chave devem obter keys/me/ e reter a mensagem até que a chave chegue.

Anexos criptografados

Os anexos são criptografados no dispositivo, enviados como texto cifrado opaco e referenciados de dentro da mensagem criptografada.

Criptografar o arquivo

Para cada arquivo, gere uma chave aleatória de 32 bytes e um nonce aleatório de 12 bytes:

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

Envie o blob. Coloque file_key e todos os campos descritivos dentro da mensagem criptografada, nunca em uma requisição de upload.

Objeto de anexo

ChaveObrigatórioSignificado
blob_idsimID retornado pelo upload.
file_keysimBase64 da chave de arquivo de 32 bytes.
media_typesimTipo MIME do arquivo em texto claro.
file_namesimNome de exibição.
file_sizesimTamanho em texto claro, em bytes.
thumb_b64nãoMiniatura JPEG em Base64, de até 64 KiB.
duration_msnãoDuração do áudio ou do vídeo.
waveformnãoAté 96 números em [0, 1] para mensagens de voz.
sticker, sticker_pack, sticker_posenãoIdentidade da figurinha.
livenãoParte em movimento de uma Live Photo: {blob_id, file_key, media_type, file_size, duration_ms?, width?, height?}.

Os destinatários ignoram anexos sem blob_id ou file_key. As mensagens de voz do App são AAC-LC em um contêiner MP4 (audio/mp4).

Upload: requisição única (até 10 MiB)

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 é o tamanho do texto cifrado. media_kind é um de voice, image, video, file, avatar, other. ttl_days é limitado ao intervalo de 1 a 30 (padrão 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
}

Em seguida:

  1. Envie o texto cifrado com PUT para upload_url em até 15 minutos, apenas com um cabeçalho Content-Type. Não envie o cabeçalho Authorization ao armazenamento.
  2. Chame POST /api/relay/blob/{blob_id}/complete/ com RelayAuth. O retransmissor confirma o objeto e retorna {blob_id, file_size, expires_at, storage}.

Upload: multipart (até 100 MiB)

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 tem padrão de 8 MiB e pode ser solicitado entre 5 e 16 MiB. As URLs das partes são válidas por 60 minutos. Envie cada parte com PUT para a respectiva URL e registre o cabeçalho de resposta ETag; em seguida:

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

O AeroNyx App usa o upload por requisição única para até 8 MiB de texto cifrado e o upload multipart acima desse tamanho.

Download

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

O retransmissor responde 302 com um Location na rede de distribuição de conteúdo e os cabeçalhos X-AeroNyx-Blob-Size, X-AeroNyx-Blob-Media-Type e X-AeroNyx-Blob-Storage. Siga o redirecionamento você mesmo, sem encaminhar nenhum cabeçalho Authorization, e então verifique e descriptografe o blob com seu file_key. Limite o download ao tamanho esperado.

Um blob_id é uma capacidade ao portador: qualquer pessoa que o possua pode obter o texto cifrado, e é por isso que a chave trafega apenas dentro da mensagem criptografada. access_mode e a expiração são aplicados no redirecionamento do retransmissor. Não dependa deles para garantir a confidencialidade.

Exclusão de anexos

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

Somente quem fez o upload pode excluir um blob. A resposta é {"blob_id": "...", "deleted": true}.

Erros de anexos

Os erros usam {"success": false, "error": "<text>", "error_code": "<code>"}. Baseie a lógica em error_code.

HTTPerror_codeSignificado
400blob_id_invalidID de blob malformado.
400blob_missing_file_size, blob_missing_total_size, blob_missing_parts, blob_bad_parts, blob_bad_part_countRequisição de upload inválida.
400blob_not_r2, blob_multipart_complete_failedA conclusão falhou; inicie um novo upload.
401auth_requiredO blob exige RelayAuth.
403blob_not_uploader, download_forbiddenO chamador não tem permissão.
404blob_not_found, blob_not_uploadedBlob desconhecido, ou conclusão antes do término do upload.
410blob_expiredO blob expirou. Peça ao remetente que o reenvie.
413blob_too_largeAcima do limite; a resposta inclui max_bytes e chunked_max_bytes.
503blob_r2_unavailableArmazenamento temporariamente indisponível; tente novamente com backoff.

Endpoints de upload legados

POST /api/relay/blob/ (formulário multipart, até 10 MiB) e a API de sessões retomáveis em /api/relay/blob/session/ (até 100 MiB, blocos de 64 KiB a 4 MiB, sessões válidas por 24 horas) continuam disponíveis como alternativa. Novos clientes devem usar os endpoints acima.

Notificações push

O retransmissor envia notificações do Apple Push Notification service (APNs) para iOS e macOS. Clientes Android recebem mensagens somente pelo WebSocket.

Uma notificação push é enviada para relay_send e group_send quando o destinatário não tem uma conexão ativa em primeiro plano, o remetente não definiu suppress_push e o destinatário não silenciou a conversa. Edições, revogações, reações e confirmações não geram notificações push. Os payloads de push não contêm texto cifrado:

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 (com sender_pubkey) ou group_message (com group_id). As notificações de chamada usam missed_call e payloads de chamada dedicados. Ao receber uma notificação push, conecte-se e execute relay_pull.

EndpointCorpo
POST /api/relay/push/register/token (64 hex), platform (ios ou macos), bundle_id, environment (production ou sandbox), token_type opcional (alert ou voip) e provider (apns).
POST /api/relay/push/unregister/token, platform, token_type e provider opcionais.
POST /api/relay/push/mute/kind (p2p ou group), target (chave pública ou ID de grupo), muted (booleano).

Os três exigem RelayAuth. Registrar um token o transfere para a identidade chamadora.

Chamadas

A sinalização de chamadas de voz e vídeo trafega pelo mesmo WebSocket como metadados em texto claro; a mídia flui separadamente. Os quadros são call_invite, call_answer, call_reject, call_hangup e call_busy para chamadas 1:1, e group_call_invite, group_call_invite_broadcast, group_call_answer e group_call_hangup_broadcast para grupos, além de quadros de admissão para reuniões com anfitrião.

O retransmissor valida room_name em relação aos participantes: p2p_ seguido dos primeiros 16 caracteres hexadecimais de SHA256(lower_key + ":" + higher_key) para chamadas 1:1, e grp_<first 8 characters of group_id>_<first 8 hex of SHA256(group_id)> para grupos. Sinais de chamada para um destinatário offline são retidos por 120 segundos.

Limites de taxa

EscopoLimiteResposta
relay_send, group_send e push HTTPS, por identidade50 por janela curta; 100.000 por diaWebSocket: error rate_limited com retry_after. HTTPS: 429 com Retry-After.
group_send por remetente e grupo10 por janela curtaerror rate_limited, scope: "sender".
group_send por grupo50 por janela curtaerror rate_limited, scope: "group".
Reações por remetente e conversa20 por janela curtaerror rate_limited, scope: "reaction".
Solicitações de contato por remetente e destinatário3 a cada 24 horassend_rejected contact_request_rate_limited.

Aguarde pelo menos retry_after segundos antes de tentar novamente. Não reenvie em um loop contínuo: os contadores continuam correndo enquanto você tenta novamente.

Checklist de implementação

  1. Gere e armazene uma identidade Ed25519; use chaves em hexadecimal minúsculo em todos os lugares.
  2. Implemente o RelayAuth e o login, a pulsação e o presence_state do WebSocket.
  3. Implemente o envelope selado e valide-o com o vetor de referência em Central Chat HTTPS API v1.
  4. Envie com relay_send, trate durable: true como enviada e gere novamente timestamp e payload_sig nas novas tentativas.
  5. Receba relay_envelope: elimine duplicatas, verifique, descriptografe e armazene; em seguida, envie message_receipt e relay_offline_ack.
  6. Execute relay_pull após o login e ao despertar por notificação push; confirme somente após o armazenamento persistente.
  7. Leia os indicadores de privacidade do perfil e respeite-os para o status de presença e as confirmações de leitura.
  8. Criptografe os anexos no dispositivo e envie-os por presign ou multipart; mantenha file_key dentro da mensagem criptografada.
  9. Verifique a autoria antes de aplicar edições e revogações.
  10. Mantenha a busca e o histórico de mensagens no dispositivo. O retransmissor não oferece busca de conteúdo e não é um arquivo.
<!-- faq:start -->

Perguntas frequentes

O AeroNyx Chat Relay pode ler minhas mensagens?

Não. Mensagens, edições, reações, mensagens de grupo e anexos são criptografados e assinados no dispositivo do remetente antes de chegarem ao retransmissor, e as chaves nunca saem dos dispositivos das pessoas que participam da conversa. O retransmissor apenas armazena e encaminha texto cifrado.

O que o AeroNyx Chat Relay pode ver?

O retransmissor vê metadados de entrega: chaves públicas do remetente e do destinatário, IDs de grupo e de mensagem, carimbos de data/hora, tamanhos de payload, estado de entrega e de leitura, sinais de presença e de digitação, metadados de sinalização de chamadas, tamanhos de anexos e endereços IP de conexão. Ele não vê o conteúdo das mensagens, o conteúdo dos anexos nem as chaves. A lista completa está no modelo de confiança, no início desta página.

Qual criptografia o chat da AeroNyx usa?

Cada identidade é um par de chaves Ed25519. Duas pessoas derivam uma chave de chat compartilhada com X25519 e HKDF-SHA256. As mensagens 1:1 são criptografadas com XChaCha20-Poly1305 e assinadas com Ed25519. Mensagens de grupo e anexos são criptografados com AES-256-GCM. Os formatos exatos de bytes e um vetor de teste estão publicados na documentação da Central Chat HTTPS API v1.

Como fotos, vídeos e arquivos são protegidos?

Cada arquivo é criptografado no dispositivo com sua própria chave AES-256-GCM aleatória antes do upload. O armazenamento recebe apenas texto cifrado. A chave do arquivo trafega dentro da mensagem com criptografia de ponta a ponta, de modo que apenas os destinatários podem descriptografar o arquivo.

O que acontece se o destinatário estiver offline?

O retransmissor mantém as mensagens criptografadas na fila offline do destinatário por até 72 horas e as entrega quando o destinatário se reconecta. No iOS e no macOS, o destinatário também recebe uma notificação push que não contém o conteúdo da mensagem.

A AeroNyx guarda meu histórico de chat?

Não. O retransmissor é um buffer de entrega: os itens são removidos depois que o dispositivo do destinatário confirma que os armazenou. O histórico de chat e a busca ficam nos seus dispositivos.

Posso criar meu próprio cliente ou bot da AeroNyx?

Sim. Qualquer software que tenha uma identidade Ed25519 e implemente os formatos desta página pode trocar mensagens com usuários do AeroNyx App. Para uma integração mais simples de requisição/resposta sem WebSocket, use a Central Chat HTTPS API v1.

O AeroNyx Chat Relay é descentralizado?

O Chat Relay é o serviço de entrega centralizado da AeroNyx. A AeroNyx também opera uma rede de nós de código aberto (AGPL-3.0) que pode transportar o texto cifrado do chat por uma rota de dois saltos com diversidade de rede. Os dois caminhos coexistem: o retransmissor oferece entrega rápida e confiável e filas offline, e o caminho por nós é uma rota opcional que reduz o que qualquer operador individual consegue observar.

Por que minha mensagem é rejeitada com verification_required?

O destinatário só aceita mensagens de contatos. Envie uma única primeira mensagem com contact_request: true, que o destinatário verá como uma solicitação de contato. São permitidas até três solicitações de contato por destinatário em qualquer janela de 24 horas.

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

Entrega verificada em dois saltos

Para o tráfego ChatRelay autenticado elegível, a origem pode escolher um caminho de dois saltos com diversidade de rede e contabilizar a entrega somente após validar a confirmação assinada do nó terminal esperado. Os nós de retransmissão roteiam texto cifrado e não analisam o payload E2E. Consulte o modelo de evidências completo em Descoberta de nós e entrega criptografada verificada por retransmissão.

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