Intégration client AeroNyx Chat Relay

AeroNyx19 juin 20264 min de lecture31 vues

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.

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

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.

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
}

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.

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

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

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

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

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. Envoyer les chunks

http
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

http
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

http
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

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

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

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.

Découverte des nœuds et livraison chiffrée vérifiable