Intégration client AeroNyx Chat Relay
Contrat client pour blind relay, presence entre contacts mutuels, read receipts réciproques, reactions chiffrées, file offline et media chiffrés reprenables.
Contrat officiel de frames et d’API media pour les équipes App, frontend, backend et agents de code qui implémentent un client AeroNyx. Le relay route uniquement du ciphertext.
Invariant de confidentialité non négociable
Relay ne peut analyser, stocker ni déduire texte de chat, reaction, voix ou media en clair, clés, nonces, waveform, noms, transcriptions, MemChain, packet payload, DNS, destinations, URL, historique, wallet traffic ou seeds privés. Le client chiffre E2E avant transport.
Le contenu E2E se trouve dans payload_b64 et payload_sig. Metadata visible limitée à type, IDs, receiver/group, timestamps bornés, delivery state, taille/expiration, access mode et compteurs agrégés.
Invariant de presence
Seules deux identités ayant un P2PContact actif dans les deux sens peuvent consulter online et last seen. Le backend vérifie les deux directions pour empêcher le scan de public keys. Presence et last seen exact peuvent être désactivés séparément.
Un résultat caché n’inclut ni online ni last_seen_ts et utilise reason=not_mutual_contact ou reason=presence_hidden.
API de confidentialité du profil
Le client lit les profile privacy flags à la connexion et garde l’UI cohérente avec le backend. PATCH accepte l’objet privacy et les champs top-level historiques.
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
Envoyez presence_subscribe uniquement pour les contacts. Si last_seen_enabled=false, affichez « récemment » ou rien, sans reconstruire une heure précise à partir d’autres signaux.
{
"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
}
Règle réciproque des read receipts
Les read receipts sont réciproques : un utilisateur qui les désactive n’envoie pas message_read et ne voit pas ceux du peer. Si un côté les désactive ou si le contact n’est pas mutuel, backend supprime le frame. Il ne contient que des 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. Même gate pendant offline pull.
Emoji reactions
Une reaction est aussi du ciphertext E2E. Relay route par receiver ou membership, déduplique avec reaction_id et applique store-and-forward si le peer est offline. L’agrégation appartient au 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 sert à l’idempotence et à offline ACK. ACK : message_reaction_ack. Groupes : group_message_reaction, group_id, key_version.
Modèle de blob chiffré
Voix, images, vidéo et fichiers sont chiffrés avant upload. blob_id, clé, nonce, durée, waveform, nom visible et preview metadata restent dans relay_send.payload_b64, jamais en clair dans l’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 chiffré
Utilisez multipart simple pour voix courte et petite image. Le serveur accepte seulement les bytes chiffrés ; TTL 7 jours par défaut, politique 1–30. Download par capability imprévisible ou mode authenticated lié aux P2P public keys.
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 simple 10 MB ; dépassement : HTTP 413, error_code=blob_too_large, chunked_max_bytes=104857600.
Upload reprenable de blob chiffré
Au-delà de la limite simple, créez une session chunked. Le total chiffré est limité à 100 MB ; répéter un index est sûr. Sauvegardez localement upload_id, chunk_size et les indices terminés.
1. Créer une session
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. Envoyer les chunks
PUT /api/relay/blob/session/{upload_id}/chunk/{chunk_index}/
Authorization: Relay <pubkey>:<timestamp>:<signature>
Content-Type: application/octet-stream
Chunk par défaut 1 MB, maximum 4 MB ; répéter le même index remplace l’ancien et reste idempotent.
3. Reprendre après interruption
GET /api/relay/blob/session/{upload_id}/
Authorization: Relay <pubkey>:<timestamp>:<signature>
Lisez missing_chunks et envoyez seulement les indices absents. La session dure 24 heures.
4. Terminer l’upload
POST /api/relay/blob/session/{upload_id}/complete/
Authorization: Relay <pubkey>:<timestamp>:<signature>
Complete vérifie chunks et total bytes ; un retry sûr renvoie le final blob existant sans doublon.
5. Annuler l’upload
DELETE /api/relay/blob/session/{upload_id}/
Authorization: Relay <pubkey>:<timestamp>:<signature>
Télécharger un blob chiffré
Capability ne requiert pas RelayAuth : l’UUID est le bearer capability. Authenticated exige une signature et autorise uploader ou allowed_downloaders. Un blob expiré est supprimé paresseusement à l’accès.
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 |
Guide UX client
Upload simple pour les petits fichiers, chunks après max_bytes. Après redémarrage, interrogez missing_chunks. Gardez les secrets dans E2E et transformez 410 blob_expired en action de renvoi.
Ordre d’intégration pour agents
Un agent implémente dans l’ordre RelayAuth, profile privacy, UI presence, read receipts réciproques, reactions idempotentes, blob simple, reprise chunked et référence relay_send. N’ajoutez pas de recherche chat serveur : relay ne peut rechercher le plaintext.
Livraison vérifiée à deux sauts
Un ChatRelay éligible peut choisir une route diverse à deux sauts. La source compte la livraison seulement après validation du receipt signé du terminal attendu ; le middle node route uniquement le ciphertext.