Integração do cliente AeroNyx Chat Relay
Contrato cliente para blind relay, presence entre contatos mútuos, read receipts recíprocos, reactions criptografadas, fila offline e mídia retomável.
Contrato oficial de frames e API de mídia para equipes App, frontend, backend e agentes de código que criam clientes AeroNyx. O relay roteia apenas ciphertext e não interpreta conteúdo.
Invariante de privacidade
Relay não pode analisar, guardar ou inferir chat, reaction, voz ou mídia em claro, chaves, nonces, waveform, nomes, transcrições, MemChain, packet payload, DNS, destinos, URL, histórico, wallet traffic ou seeds. O client cifra E2E antes do transporte.
Conteúdo E2E fica em payload_b64 e payload_sig. Metadata visível limita-se a type, IDs, receiver/group, timestamps, delivery state, blob size/expiry, access mode e contadores.
Invariante de presence
Online e last seen só são visíveis quando P2PContact existe nos dois sentidos. Backend verifica ambas as direções para impedir scan de public keys. Presence e last seen exato podem ser desligados separadamente.
Resultado oculto não inclui online nem last_seen_ts; usa reason=not_mutual_contact ou reason=presence_hidden.
API de privacidade do perfil
O client lê profile privacy flags ao conectar e mantém UI e backend alinhados. PATCH aceita privacy aninhado e os antigos campos top-level.
GET /api/relay/profile/
PATCH /api/relay/profile/
Authorization: Relay <pubkey>:<timestamp>:<signature>
{
"privacy": {
"presence_enabled": true,
"last_seen_enabled": false,
"read_receipts_enabled": false
}
}
Frames de presence
Envie presence_subscribe somente para contatos. Com last_seen_enabled=false, mostre estado aproximado ou esconda o horário; não deduza um valor exato por outros sinais.
{
"type": "presence_subscribe",
"pubkeys": ["contact-pubkey-a", "contact-pubkey-b"]
}
{
"type": "presence_subscribe_ack",
"updates": [{
"pubkey": "contact-pubkey-a",
"visible": true,
"presence_visible": true,
"last_seen_visible": true,
"online": true,
"last_seen_ts": 1780000000,
"reason": "allowed"
}],
"server_ts": 1780000001
}
Regra recíproca de read receipts
Read receipts são recíprocos: quem desliga não envia message_read nem vê o peer read. Se um lado desligar ou não houver contato mútuo, backend suprime o frame. É metadata only.
{
"type": "message_read",
"msg_id": "message-id",
"receiver_pubkey": "original-sender-pubkey",
"timestamp": 1780000200
}
{
"type": "message_read_ack",
"msg_id": "message-id",
"delivered": false,
"suppressed": true,
"reason": "receiver_read_receipts_disabled"
}
Reasons: client_disabled, not_mutual_contact, reader_read_receipts_disabled, receiver_read_receipts_disabled; mesma gate no offline pull.
Emoji reactions
Reaction também é ciphertext E2E. Relay roteia por receiver ou membership, deduplica por reaction_id e usa store-and-forward offline. A agregação pertence ao client.
{
"type": "message_reaction",
"msg_id": "target-message-id",
"receiver_pubkey": "peer-pubkey",
"reaction_id": "unique-reaction-event-id",
"timestamp": 1780000300,
"payload_b64": "e2e-ciphertext",
"payload_sig": "ed25519-signature"
}
Discriminant 12; reaction_id é idempotency/offline ACK key. ACK message_reaction_ack; grupos usam group_message_reaction, group_id, key_version.
Modelo de blob criptografado
Voz, imagens, vídeo e arquivos são criptografados antes do upload. blob_id, key, nonce, duração, waveform, nome e preview ficam em relay_send.payload_b64, nunca em campos claros da blob API.
{
"kind": "voice",
"blob_id": "blob-uuid",
"key_b64": "inside-e2e-envelope",
"nonce_b64": "inside-e2e-envelope",
"duration_ms": 43000,
"waveform": [0, 3, 8, 6, 2],
"media_type": "audio/ogg; codecs=opus",
"file_size": 7340032
}
Upload simples de blob
Use multipart simples para voz curta e imagem pequena. O servidor aceita apenas bytes criptografados; TTL padrão 7 dias, política 1–30. Download por capability imprevisível ou authenticated por P2P public key.
POST /api/relay/blob/
Authorization: Relay <pubkey>:<timestamp>:<signature>
Content-Type: multipart/form-data
| field | required | value |
|---|---|---|
file | true | ciphertext |
media_kind | false | voice, image, video, file, avatar, other |
media_type | false | MIME |
ttl_days | false | 1..30 |
access_mode | false | capability, authenticated |
allowed_downloaders | false | JSON P2P pubkey array |
Limite simples 10 MB; excesso: HTTP 413, error_code=blob_too_large, chunked_max_bytes=104857600.
Upload retomável de blob
Acima do limite simples, use sessão chunked. O total cifrado máximo é 100 MB e repetir chunk index é seguro. Salve upload_id, chunk_size e índices completos localmente.
1. Criar sessão
POST /api/relay/blob/session/
Authorization: Relay <pubkey>:<timestamp>:<signature>
Content-Type: application/json
{
"total_size": 7340032,
"chunk_size": 1048576,
"media_type": "audio/ogg",
"media_kind": "voice",
"ttl_days": 7,
"access_mode": "authenticated",
"allowed_downloaders": ["receiver-pubkey"]
}
2. Enviar chunks
PUT /api/relay/blob/session/{upload_id}/chunk/{chunk_index}/
Authorization: Relay <pubkey>:<timestamp>:<signature>
Content-Type: application/octet-stream
Chunk padrão 1 MB, máximo 4 MB; retry do mesmo index substitui o anterior e é idempotente.
3. Retomar após interrupção
GET /api/relay/blob/session/{upload_id}/
Authorization: Relay <pubkey>:<timestamp>:<signature>
Leia missing_chunks e envie só o que falta. A sessão dura 24 horas.
4. Completar upload
POST /api/relay/blob/session/{upload_id}/complete/
Authorization: Relay <pubkey>:<timestamp>:<signature>
Complete verifica chunks e total bytes; retry seguro devolve o final blob existente sem duplicar.
5. Cancelar upload
DELETE /api/relay/blob/session/{upload_id}/
Authorization: Relay <pubkey>:<timestamp>:<signature>
Download do blob criptografado
Capability dispensa RelayAuth porque o UUID é bearer capability. Authenticated exige assinatura e autoriza uploader ou allowed_downloaders. Blob expirado é removido ao acesso.
GET /api/relay/blob/{blob_id}/
| HTTP | error_code |
|---|---|
| 400 | blob_id_invalid |
| 401 | auth_required |
| 403 | download_forbidden |
| 404 | blob_not_found |
| 410 | blob_expired |
| 413 | blob_too_large, blob_total_size_too_large, chunk_too_large |
| 409 | upload_incomplete |
Guia UX do cliente
Use upload simples para arquivo pequeno e chunks após max_bytes. Depois de reiniciar consulte missing_chunks. Mantenha secrets no E2E e trate 410 blob_expired como ação de reenviar.
Ordem de integração para agentes
Agentes implementam RelayAuth, profile privacy, presence UI, read receipts recíprocos, reaction idempotente, blob simples, resume chunked e referência relay_send. Não criam busca de chat no servidor.
Entrega verificada em dois saltos
ChatRelay elegível pode usar rota diversa de dois saltos. A source conta delivery somente após verificar receipt assinado do terminal esperado; middle node só roteia ciphertext.