Integración del cliente AeroNyx Chat Relay

AeroNyx19 de junio de 20264 min de lectura31 vistas

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.

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

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.

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
}

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.

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

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

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.

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

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

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

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. Subir chunks

http
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

http
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

http
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

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

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

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.

Descubrimiento de nodos y entrega cifrada verificable