Integração do cliente AeroNyx Chat Relay

AeroNyx19 de junho de 20264 min de leitura29 visualizações

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.

http
GET /api/relay/profile/
PATCH /api/relay/profile/
Authorization: Relay <pubkey>:<timestamp>:<signature>
json
{
  "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.

json
{
  "type": "presence_subscribe",
  "pubkeys": ["contact-pubkey-a", "contact-pubkey-b"]
}
json
{
  "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.

json
{
  "type": "message_read",
  "msg_id": "message-id",
  "receiver_pubkey": "original-sender-pubkey",
  "timestamp": 1780000200
}
json
{
  "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.

json
{
  "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.

json
{
  "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.

http
POST /api/relay/blob/
Authorization: Relay <pubkey>:<timestamp>:<signature>
Content-Type: multipart/form-data
fieldrequiredvalue
filetrueciphertext
media_kindfalsevoice, image, video, file, avatar, other
media_typefalseMIME
ttl_daysfalse1..30
access_modefalsecapability, authenticated
allowed_downloadersfalseJSON 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

http
POST /api/relay/blob/session/
Authorization: Relay <pubkey>:<timestamp>:<signature>
Content-Type: application/json
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

http
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

http
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

http
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

http
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.

http
GET /api/relay/blob/{blob_id}/
HTTPerror_code
400blob_id_invalid
401auth_required
403download_forbidden
404blob_not_found
410blob_expired
413blob_too_large, blob_total_size_too_large, chunk_too_large
409upload_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.

Descoberta de nós e entrega criptografada verificável