Integração de clientes com o AeroNyx Chat Relay
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_requeste 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:
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
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
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:
{
"type": "auth",
"pubkey": "<64 hex>",
"timestamp": 1780000000,
"signature": "<128 hex>"
}
Sucesso:
{ "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ção | Quadro | Comportamento |
|---|---|---|
| servidor → cliente | {"type":"ping"} a cada 30 s | Responda {"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"}. Mantenhapayload_b64abaixo de cerca de 800 KiB para deixar espaço para o restante do quadro. - Quadros malformados retornam
errorcom o motivoinvalid_json,invalid_json_typeouunknown_type.
Quadro de erro genérico:
{ "type": "error", "reason": "<reason>", "retry_after": 0 }
Erros de limite de taxa também incluem scope.
Códigos de fechamento
| Código | Significado |
|---|---|
1008 | Host ou Origin não permitido. |
4000 | O servidor não conseguiu enviar sua pulsação. |
4001 | O login falhou. |
4002 | O 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:
{
"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:
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
{
"type": "relay_send",
"msg_id": "9e762ae0f5da43e6bec5eb8143f1f834",
"receiver_pubkey": "<64 hex>",
"discriminant": 11,
"payload_b64": "<base64 envelope>",
"timestamp": 1780000000,
"payload_sig": "<128 hex>"
}
| Campo | Regras |
|---|---|
msg_id | 32 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_pubkey | A chave pública do destinatário. |
discriminant | 11. |
timestamp | Segundos Unix, dentro de ±300 s do horário do servidor. |
suppress_push | Opcional. true não envia nenhuma notificação push. |
contact_request | Opcional. Marca uma primeira mensagem para alguém que exige verificação de contato (consulte Verificação de contato). |
4. Confirmação de recebimento
{ "type": "relay_delivered", "msg_id": "...", "delivered": true, "queued": true, "durable": true }
durable(equeued) étrueassim que a mensagem é armazenada na fila offline do destinatário. Tratedurable: truecomo "enviada".deliveredsignifica 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
{ "type": "send_rejected", "msg_id": "...", "reason": "verification_required" }
| Resposta | Motivo | Ação |
|---|---|---|
send_rejected | verification_required | O destinatário aceita mensagens apenas de contatos. Não tente novamente. |
send_rejected | contact_request_rate_limited | Limite de solicitações de contato atingido para este destinatário. Não tente novamente. |
error | missing_fields | Um campo obrigatório está ausente ou vazio. |
error | invalid_timestamp, timestamp_expired | Corrija o relógio e gere um novo carimbo de data/hora. |
error | invalid_payload_sig | payload_sig não passa na verificação. |
error | rate_limited | Tente 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:
POST /api/relay/push/
Authorization: Relay <pubkey>:<timestamp>:<signature>
Content-Type: application/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:
{
"type": "relay_envelope",
"sender_pubkey": "<64 hex>",
"discriminant": 11,
"payload_b64": "<base64 envelope>",
"timestamp": 1780000000,
"msg_id": "9e762ae0f5da43e6bec5eb8143f1f834",
"from_offline": false
}
Para discriminant: 11:
- Elimine duplicatas por
msg_id. A mesma mensagem pode chegar ao vivo e novamente pela fila offline. - Analise o envelope e verifique se o
receiver_pubkeydele é a sua identidade. - 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. - Verifique a assinatura do envelope com o
sender_pubkeydo envelope. Essa chave, e não osender_pubkeydo quadro, é o remetente autenticado. - 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.
- 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:
{ "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:
{ "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:
{ "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:
| Limite | Valor |
|---|---|
| Itens por destinatário | 1.000. Quando a fila está cheia, novos itens são rejeitados e o remetente recebe durable: false. |
| Retenção | 72 horas após a inclusão do item mais recente. |
| Tamanho do payload | 1 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:
{ "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
{ "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:
{ "type": "message_read_ack", "msg_id": "...", "delivered": false, "suppressed": true, "reason": "receiver_read_receipts_disabled" }
| Motivo | Significado |
|---|---|
client_disabled | O quadro continha enabled: false ou read_receipts_enabled: false. |
not_mutual_contact | Os usuários não são contatos mútuos. |
reader_read_receipts_disabled | O leitor desativou as confirmações de leitura. |
receiver_read_receipts_disabled | O remetente original desativou as confirmações de leitura. |
invalid_pubkey | Uma 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:
{
"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
{
"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:
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"):
{
"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_sigusa o discriminante12.reaction_idé a chave de idempotência. Umreaction_idrepetido em até 72 horas é confirmado com"duplicate": truee 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:
{ "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.
{
"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:
{ "type": "presence_update", "pubkey": "<a>", "online": true, "presence_visible": true, "last_seen_visible": true, "last_seen_ts": 1780000100 }
Digitação
{ "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
GET /api/relay/profile/
Authorization: Relay <pubkey>:<timestamp>:<signature>
{
"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.
PATCH /api/relay/profile/
Authorization: Relay <pubkey>:<timestamp>:<signature>
Content-Type: application/json
{ "display_name": "Alice", "privacy": { "presence_enabled": true, "last_seen_enabled": false, "read_receipts_enabled": false } }
| Campo | Regras |
|---|---|
display_name | Até 50 caracteres. |
bio | Até 200 caracteres. |
avatar_url | URL https:// ou vazio. |
handle | a-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:
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.
{
"type": "group_send",
"msg_id": "<32 hex>",
"group_id": "<uuid>",
"payload_b64": "<base64>",
"timestamp": 1780000000,
"payload_sig": "<128 hex>",
"key_version": 3
}
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.
{
"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
| Quadro | Assinatura |
|---|---|
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:
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
| Chave | Obrigatório | Significado |
|---|---|---|
blob_id | sim | ID retornado pelo upload. |
file_key | sim | Base64 da chave de arquivo de 32 bytes. |
media_type | sim | Tipo MIME do arquivo em texto claro. |
file_name | sim | Nome de exibição. |
file_size | sim | Tamanho em texto claro, em bytes. |
thumb_b64 | não | Miniatura JPEG em Base64, de até 64 KiB. |
duration_ms | não | Duração do áudio ou do vídeo. |
waveform | não | Até 96 números em [0, 1] para mensagens de voz. |
sticker, sticker_pack, sticker_pose | não | Identidade da figurinha. |
live | não | Parte 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)
POST /api/relay/blob/presign/
Authorization: Relay <pubkey>:<timestamp>:<signature>
Content-Type: application/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).
{
"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:
- Envie o texto cifrado com
PUTparaupload_urlem até 15 minutos, apenas com um cabeçalhoContent-Type. Não envie o cabeçalhoAuthorizationao armazenamento. - 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)
POST /api/relay/blob/multipart/create/
{ "total_size": 52428800, "media_type": "video/mp4", "media_kind": "video", "ttl_days": 7 }
{
"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:
POST /api/relay/blob/multipart/complete/
{ "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
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
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.
| HTTP | error_code | Significado |
|---|---|---|
| 400 | blob_id_invalid | ID de blob malformado. |
| 400 | blob_missing_file_size, blob_missing_total_size, blob_missing_parts, blob_bad_parts, blob_bad_part_count | Requisição de upload inválida. |
| 400 | blob_not_r2, blob_multipart_complete_failed | A conclusão falhou; inicie um novo upload. |
| 401 | auth_required | O blob exige RelayAuth. |
| 403 | blob_not_uploader, download_forbidden | O chamador não tem permissão. |
| 404 | blob_not_found, blob_not_uploaded | Blob desconhecido, ou conclusão antes do término do upload. |
| 410 | blob_expired | O blob expirou. Peça ao remetente que o reenvie. |
| 413 | blob_too_large | Acima do limite; a resposta inclui max_bytes e chunked_max_bytes. |
| 503 | blob_r2_unavailable | Armazenamento 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:
{
"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.
| Endpoint | Corpo |
|---|---|
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
| Escopo | Limite | Resposta |
|---|---|---|
relay_send, group_send e push HTTPS, por identidade | 50 por janela curta; 100.000 por dia | WebSocket: error rate_limited com retry_after. HTTPS: 429 com Retry-After. |
group_send por remetente e grupo | 10 por janela curta | error rate_limited, scope: "sender". |
group_send por grupo | 50 por janela curta | error rate_limited, scope: "group". |
| Reações por remetente e conversa | 20 por janela curta | error rate_limited, scope: "reaction". |
| Solicitações de contato por remetente e destinatário | 3 a cada 24 horas | send_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
- Gere e armazene uma identidade Ed25519; use chaves em hexadecimal minúsculo em todos os lugares.
- Implemente o RelayAuth e o login, a pulsação e o
presence_statedo WebSocket. - Implemente o envelope selado e valide-o com o vetor de referência em Central Chat HTTPS API v1.
- Envie com
relay_send, tratedurable: truecomo enviada e gere novamentetimestampepayload_signas novas tentativas. - Receba
relay_envelope: elimine duplicatas, verifique, descriptografe e armazene; em seguida, enviemessage_receipterelay_offline_ack. - Execute
relay_pullapós o login e ao despertar por notificação push; confirme somente após o armazenamento persistente. - Leia os indicadores de privacidade do perfil e respeite-os para o status de presença e as confirmações de leitura.
- Criptografe os anexos no dispositivo e envie-os por
presignou multipart; mantenhafile_keydentro da mensagem criptografada. - Verifique a autoria antes de aplicar edições e revogações.
- 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.
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.
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 -->