Integración del cliente AeroNyx Chat Relay
Contrato cliente para blind relay, presence entre contactos mutuos, read receipts recíprocos, reactions cifradas, cola offline y media cifrada reanudable.
Contrato oficial de frames y API de media para equipos App, frontend, backend y agentes de código que crean clientes AeroNyx. El relay solo enruta ciphertext y no interpreta el contenido.
Invariante de privacidad no negociable
Relay no puede analizar, guardar ni inferir texto de chat, reactions, voz o media en claro, claves, nonces, waveform, nombres, transcripciones, MemChain, packet payload, DNS, destinos, URL, historial, wallet traffic ni seeds privados. El cliente cifra E2E antes del transporte.
El contenido E2E está en payload_b64 y payload_sig. La metadata visible se limita a type, IDs, receiver/group, timestamps acotados, estado de entrega, tamaño/expiración, access mode y contadores agregados.
Invariante de presence
Solo dos identidades con P2PContact activo en ambas direcciones pueden consultar online y last seen. El backend comprueba ambos sentidos para impedir el escaneo de public keys. Cada usuario puede desactivar presence y last seen exacto por separado.
Un resultado oculto no incluye online ni last_seen_ts; usa reason=not_mutual_contact o reason=presence_hidden.
API de privacidad del perfil
El cliente lee los profile privacy flags al conectarse y mantiene la UI alineada con el backend. PATCH acepta el objeto privacy y conserva campos top-level para integraciones anteriores.
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
Envía presence_subscribe solo para contactos. Si last_seen_enabled=false, muestra un estado aproximado como «recientemente» o no muestres hora; no infieras un valor exacto mediante otras señales.
{
"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
}
Regla recíproca de read receipts
Los read receipts son recíprocos: quien los desactiva no envía message_read ni ve los del peer. Si un lado los desactiva o no hay contacto mutuo, backend suprime el frame. Solo contiene metadata.
{
"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. La misma puerta se aplica al offline pull.
Emoji reactions
Una reaction también es ciphertext E2E. Relay la enruta por receiver o membership, deduplica con reaction_id y usa store-and-forward si el peer está offline. El estado agregado pertenece al cliente.
{
"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"
}
El discriminant es 12; reaction_id es la clave de idempotencia y offline ACK. ACK: message_reaction_ack. Grupos: group_message_reaction, group_id, key_version.
Modelo de blob cifrado
Voz, imágenes, video y archivos se cifran antes de subir. blob_id, clave, nonce, duración, waveform, nombre visible y preview metadata permanecen dentro de relay_send.payload_b64, nunca como campos claros de la API blob.
{
"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 simple de blob cifrado
Usa multipart simple para voz corta e imágenes pequeñas. El servidor acepta solo bytes cifrados; TTL predeterminado 7 días, política 1–30. La descarga puede ser capability no adivinable o 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 |
Límite simple 10 MB; al excederlo: HTTP 413, error_code=blob_too_large, chunked_max_bytes=104857600.
Upload reanudable de blob cifrado
Cuando el archivo supera el límite simple, crea una sesión por chunks. El total cifrado máximo es 100 MB; repetir un chunk index es seguro. Guarda localmente upload_id, chunk_size e índices completados.
1. Crear sesión
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. Subir chunks
PUT /api/relay/blob/session/{upload_id}/chunk/{chunk_index}/
Authorization: Relay <pubkey>:<timestamp>:<signature>
Content-Type: application/octet-stream
Chunk predeterminado 1 MB, máximo 4 MB; repetir el mismo index reemplaza el anterior y es idempotente.
3. Reanudar tras una interrupción
GET /api/relay/blob/session/{upload_id}/
Authorization: Relay <pubkey>:<timestamp>:<signature>
Lee missing_chunks y envía solo los índices faltantes. La sesión dura 24 horas.
4. Completar upload
POST /api/relay/blob/session/{upload_id}/complete/
Authorization: Relay <pubkey>:<timestamp>:<signature>
Complete verifica todos los chunks y bytes; un retry seguro devuelve el final blob existente sin duplicarlo.
5. Cancelar upload
DELETE /api/relay/blob/session/{upload_id}/
Authorization: Relay <pubkey>:<timestamp>:<signature>
Descargar blob cifrado
Capability no necesita RelayAuth porque el UUID actúa como bearer capability. Authenticated exige firma y limita el acceso al uploader o allowed_downloaders. Los blobs vencidos se eliminan de forma perezosa al acceder.
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 |
Guía UX del cliente
Usa upload simple para archivos pequeños y chunks solo al superar max_bytes. Tras reiniciar la App consulta missing_chunks. Mantén todos los secretos en E2E y convierte 410 blob_expired en una acción de reenvío.
Orden de integración para agentes
Un agente debe implementar RelayAuth, profile privacy, UI de presence, read receipts recíprocos, reactions idempotentes, blob simple, resume por chunks y referencia en relay_send. No debe crear búsqueda de chat en servidor: relay no puede buscar plaintext cifrado.
Entrega verificada de dos saltos
El tráfico ChatRelay elegible puede elegir una ruta diversa de dos saltos. El origen cuenta la entrega solo tras verificar el receipt firmado del terminal esperado; el nodo intermedio solo enruta ciphertext.