Integrasi Klien AeroNyx Chat Relay
Referensi integrasi untuk AeroNyx Chat Relay: autentikasi WebSocket, pesan 1:1 dan grup yang tersegel, pengiriman luring, tanda terima, reaksi, status kehadiran, lampiran terenkripsi, notifikasi push, dan batas laju.
Buat instruksi integrasi
Pilih teknologi klien dan tugas, lalu salin ke asisten pemrograman. Instruksi dibuat secara lokal, tanpa dikirim ke layanan AI.
Instruksi yang dibuat
Ini adalah referensi integrasi untuk AeroNyx Chat Relay: layanan WebSocket dan HTTPS di api.aeronyx.network yang menyalurkan pesan 1:1 terenkripsi ujung ke ujung, pesan grup, tanda terima, reaksi, status kehadiran, dan lampiran terenkripsi antaridentitas AeroNyx.
Dokumen ini ditujukan bagi engineer yang membangun klien, bot, dan layanan yang kompatibel dengan AeroNyx, serta bagi agen AI pemrograman yang mengimplementasikannya. Setiap frame, field, dan batas pada halaman ini mencerminkan relay produksi dan AeroNyx App per Oktober 2026.
Chat Relay adalah jalur pengiriman terpusat. Jalur ini berdampingan dengan jalur node terdesentralisasi (onion routing dan kotak surat anonim, lihat Pengiriman dua-hop terverifikasi); sebuah klien dapat menggunakan keduanya. Untuk integrasi HTTPS berbasis permintaan/respons tanpa WebSocket, lihat Central Chat HTTPS API v1.
Model kepercayaan
Relay tidak dapat melihat konten. Klien mengenkripsi dan menandatangani semua data sebelum mencapai relay, sedangkan relay hanya merutekan, mengantrekan, dan menerapkan batas laju pada ciphertext yang tidak dapat dibaca.
Relay tidak pernah menerima:
- teks pesan, emoji reaksi, suntingan, atau payload grup dalam bentuk plaintext
- kunci chat, kunci grup, kunci lampiran, atau nonce
- isi lampiran, nama file, thumbnail, bentuk gelombang suara, atau transkrip
- kunci identitas privat
Relay tetap mengamati metadata pengiriman, dan integrator harus memperlakukannya sebagai informasi yang terlihat oleh operator:
- kunci publik pengirim dan penerima, ID grup, dan ID pesan
- stempel waktu, ukuran payload, serta status pengiriman, tanda terima, dan status baca
- status kehadiran, status latar depan, dan indikator mengetik (dikirim sebagai frame plaintext)
- metadata pensinyalan panggilan (nama ruang, ID panggilan, flag video)
- ukuran ciphertext lampiran, tipe media yang dideklarasikan, dan masa kedaluwarsa
- flag
contact_requestdan metadata koneksi tingkat IP
Kerahasiaan konten berasal dari enkripsi ujung ke ujung, bukan dari kontrol akses pada relay. Rancang sistem Anda dengan prinsip ini: penyerang yang memperoleh ciphertext tersimpan tetap tidak boleh dapat membacanya.
Identitas dan kunci
Identitas chat AeroNyx adalah pasangan kunci Ed25519. Kunci publik 32 byte, yang ditulis sebagai 64 karakter heksadesimal huruf kecil, berfungsi sebagai alamat. Buat kunci di perangkat dan jangan pernah mengirim kunci privat ke mana pun.
Dua kunci diturunkan dari pasangan identitas:
- Kunci chat (1:1).
HKDF-SHA256(salt = empty, ikm = X25519(my_secret, peer_public), info = "AERONYX-P2P-KEY", length = 32), dengan kedua kunci Ed25519 dikonversi ke X25519 (SHA-512(seed)[0..32]yang di-clamp untuk kunci rahasia, konversi Edwards ke Montgomery untuk kunci publik). Kedua pihak menurunkan kunci yang sama. - Tanda tangan pesan. Ed25519 dengan kunci identitas, atas string byte persis seperti yang didefinisikan di halaman ini.
Selalu gunakan hex huruf kecil untuk kunci publik di dalam frame. Relay tidak menormalkan huruf besar/kecil pada setiap kunci antrean.
Autentikasi
Tanda tangan RelayAuth
Login WebSocket dan setiap endpoint HTTPS yang memerlukan autentikasi menggunakan tanda tangan yang sama:
digest = SHA256("AeroNyx-RelayAuth-v1" || pubkey[32] || timestamp as u64 little-endian)
signature = Ed25519(identity_key, digest)
"AeroNyx-RelayAuth-v1" adalah 20 byte ASCII tanpa karakter terminator. timestamp dinyatakan dalam detik Unix dan harus berada dalam rentang 300 detik dari waktu server.
Tanda tangan ini hanya mengikat identitas dan waktu. Tanda tangan tidak mengikat metode, path, body, maupun koneksi, dan tidak ada nonce. Buat stempel waktu baru untuk setiap permintaan, kirim hanya melalui TLS, dan jangan pernah mencatatnya di log.
Header HTTPS
Authorization: Relay <pubkey-hex>:<timestamp>:<signature-hex>
Pemeriksaan yang gagal mengembalikan HTTP 401 dengan {"success": false, "error": "<reason>"}. Alasan yang mungkin antara lain missing_auth_header, malformed_auth_header, invalid_timestamp, timestamp_expired, invalid_pubkey, dan invalid_signature.
Koneksi WebSocket
Endpoint
wss://api.aeronyx.network/ws/relay/
Klien native terhubung tanpa header Origin. Browser harus terhubung dari origin yang diizinkan; origin lain, atau Host yang tidak dikenal, akan ditutup dengan kode 1008.
Login
Server menerima socket, lalu menunggu frame auth dalam waktu 30 detik:
{
"type": "auth",
"pubkey": "<64 hex>",
"timestamp": 1780000000,
"signature": "<128 hex>"
}
Berhasil:
{ "type": "auth_ack", "session_id": "<uuid>", "server_ts": 1780000001 }
Gunakan server_ts untuk memperkirakan selisih jam; stempel waktu pesan diperiksa terhadap jendela ±300 detik yang sama.
Setelah auth_ack, server memulai heartbeat, melanggankan koneksi ke kanal-kanalnya, dan segera memutar ulang antrean luring (lihat Pengiriman luring).
Jika gagal, server mengirim {"type": "auth_error", "reason": "<reason>"} dan menutup socket dengan kode 4001. Alasan: missing_fields, timestamp_expired, invalid_pubkey, invalid_signature_length, invalid_signature_encoding, invalid_signature, internal_error. Jika batas waktu login terlampaui, server mengirim alasan timeout dan menutup dengan kode 4002.
Frame lain apa pun yang dikirim sebelum login dijawab dengan auth_error beralasan authentication_required; socket tetap terbuka.
Heartbeat dan status latar depan
| Arah | Frame | Perilaku |
|---|---|---|
| server → klien | {"type":"ping"} setiap 30 detik | Balas dengan {"type":"pong"}. |
| klien → server | {"type":"ping"} | Server membalas {"type":"pong"}. |
| klien → server | {"type":"presence_state","foreground":true} | Menandai koneksi ini sebagai aktif. |
| klien → server | {"type":"presence_state","foreground":false} | Menandai koneksi berada di latar belakang, sehingga relay dapat mengirim push untuk pesan baru. |
Relay menganggap sebuah identitas sedang online hanya selama koneksinya mengirim ping, pong, atau presence_state dengan foreground: true setidaknya setiap 90 detik. AeroNyx App mengirim ping setiap 15 detik saat berada di latar depan dan menganggap koneksi terputus jika lebih dari tiga pong tidak diterima.
Koneksi berumur panjang sebaiknya tersambung ulang setidaknya sekali setiap 24 jam. Pesan tidak pernah hilang ketika koneksi secara diam-diam berhenti menerima frame langsung, karena setiap pesan juga dimasukkan ke antrean dan diputar ulang saat login, tetapi pengiriman langsung baru berlanjut setelah koneksi tersambung ulang.
Aturan frame
- Hanya frame teks, satu objek JSON per frame. Frame biner diabaikan.
- Ukuran frame maksimum adalah 1.048.576 karakter. Frame yang lebih besar ditolak dengan
{"type":"error","reason":"message_too_large"}. Jagapayload_b64tetap di bawah sekitar 800 KiB agar tersisa ruang untuk bagian frame lainnya. - Frame dengan format tidak valid mengembalikan
errordengan alasaninvalid_json,invalid_json_type, atauunknown_type.
Frame error generik:
{ "type": "error", "reason": "<reason>", "retry_after": 0 }
Error batas laju juga menyertakan scope.
Kode penutupan
| Kode | Arti |
|---|---|
1008 | Host atau Origin tidak diizinkan. |
4000 | Server tidak dapat mengirim heartbeat-nya. |
4001 | Login gagal. |
4002 | Batas waktu login terlampaui. |
Sambungkan ulang dengan exponential backoff dan jitter. AeroNyx App menunggu 2^(attempt-1) detik, dibatasi pada rentang 1–60 detik, lalu dikalikan dengan faktor acak antara 0,8 dan 1,2.
Mengirim pesan 1:1
1. Bangun amplop tersegel
Payload pesan 1:1 adalah ChatEnvelope yang ditandatangani dan dienkripsi. Konstruksi persisnya, implementasi referensi dalam Python, dan vektor uji acuan tersedia di Central Chat HTTPS API v1: Format amplop tersegel. Amplop yang sama digunakan pada kedua API.
Ringkasnya: XChaCha20-Poly1305 dengan kunci chat, tanda tangan Ed25519 atas transkrip 121 byte, dan tata letak biner yang tetap. content_type bernilai 0 untuk setiap pesan, termasuk pesan yang memiliki lampiran.
Plaintext berupa teks UTF-8 untuk pesan biasa, atau objek JSON untuk pesan dengan lampiran, balasan, penerusan, atau pratinjau tautan:
{
"type": "aeronyx_message",
"text": "See the attached file",
"attachments": [ { "blob_id": "...", "file_key": "...", "media_type": "image/jpeg", "file_name": "photo.jpg", "file_size": 482113 } ],
"reply": { "msg_id": "...", "sender_pubkey": "...", "text": "..." },
"forwarded": true,
"forwarded_from_name": "...",
"forwarded_from_pubkey": "...",
"link_preview": { "url": "https://...", "title": "...", "description": "...", "site_name": "..." }
}
Semua field selain type dan text bersifat opsional. Penerima sebaiknya menampilkan plaintext apa pun yang bukan objek JSON dengan "type": "aeronyx_message" sebagai teks biasa. Objek lampiran didefinisikan di bagian Lampiran terenkripsi.
2. Tandatangani frame
Setiap frame pesan membawa tanda tangan kedua, payload_sig, yang diverifikasi oleh relay sebelum frame diterima:
payload_sig = Ed25519(identity_key,
SHA256(receiver_pubkey[32] || sender_pubkey[32] || discriminant as 1 byte
|| timestamp as u64 little-endian || envelope_bytes))
envelope_bytes adalah hasil dekode payload_b64. Gunakan discriminant 11 untuk pesan dan suntingan, serta 12 untuk reaksi. payload_sig dienkode dalam hex.
3. Kirim
{
"type": "relay_send",
"msg_id": "9e762ae0f5da43e6bec5eb8143f1f834",
"receiver_pubkey": "<64 hex>",
"discriminant": 11,
"payload_b64": "<base64 envelope>",
"timestamp": 1780000000,
"payload_sig": "<128 hex>"
}
| Field | Aturan |
|---|---|
msg_id | 32 karakter hex huruf kecil: message_id 16 byte milik amplop. App penerima menyimpan pesan dengan ID ini, sehingga nilainya harus sama dengan amplop. |
receiver_pubkey | Kunci publik penerima. |
discriminant | 11. |
timestamp | Detik Unix, dalam rentang ±300 detik dari waktu server. |
suppress_push | Opsional. true berarti notifikasi push tidak dikirim. |
contact_request | Opsional. Menandai pesan pertama kepada pengguna yang mewajibkan verifikasi kontak (lihat Verifikasi kontak). |
4. Pengakuan
{ "type": "relay_delivered", "msg_id": "...", "delivered": true, "queued": true, "durable": true }
durable(danqueued) bernilaitruesetelah pesan tersimpan di antrean luring penerima. Anggapdurable: truesebagai "terkirim".deliveredberarti penerima memiliki koneksi latar depan yang aktif. Ini bukan bukti penerimaan; gunakan tanda terima pengiriman untuk keperluan tersebut.
Tunggu pengakuan hingga 15 detik sebelum menganggap percobaan pengiriman gagal.
Penolakan dan error
{ "type": "send_rejected", "msg_id": "...", "reason": "verification_required" }
| Respons | Alasan | Tindakan |
|---|---|---|
send_rejected | verification_required | Penerima hanya menerima pesan dari kontak. Jangan coba ulang. |
send_rejected | contact_request_rate_limited | Batas permintaan kontak untuk penerima ini telah tercapai. Jangan coba ulang. |
error | missing_fields | Field wajib tidak ada atau kosong. |
error | invalid_timestamp, timestamp_expired | Perbaiki jam, lalu buat ulang stempel waktu. |
error | invalid_payload_sig | payload_sig gagal diverifikasi. |
error | rate_limited | Coba ulang setelah retry_after detik. |
Percobaan ulang
Coba ulang pesan yang belum diakui dengan msg_id yang sama dan byte amplop yang sama. Karena relay menolak frame yang lebih lama dari 300 detik, hitung timestamp frame dan payload_sig yang baru untuk setiap percobaan ulang; amplop tetap mempertahankan stempel waktu aslinya. Relay dan penerima melakukan deduplikasi berdasarkan msg_id.
Fallback HTTPS
Saat WebSocket tidak tersedia, pesan yang sama dapat dikirim melalui HTTPS:
POST /api/relay/push/
Authorization: Relay <pubkey>:<timestamp>:<signature>
Content-Type: application/json
{
"receiver_pubkey": "<64 hex>",
"discriminant": 11,
"payload_b64": "<base64 envelope>",
"timestamp": 1780000000,
"payload_sig": "<128 hex>"
}
Keberhasilan ditandai dengan HTTP 200 dan {"success": true}. Kegagalan verifikasi kontak mengembalikan 400 dengan verification_required atau contact_request_rate_limited, sedangkan pembatasan laju mengembalikan 429 dengan Retry-After. Pesan yang dikirim dengan cara ini dimasukkan ke antrean penerima tetapi tidak memicu notifikasi push, jadi kirim ulang melalui WebSocket setelah koneksi tersambung kembali.
Menerima pesan
Pesan masuk tiba sebagai relay_envelope:
{
"type": "relay_envelope",
"sender_pubkey": "<64 hex>",
"discriminant": 11,
"payload_b64": "<base64 envelope>",
"timestamp": 1780000000,
"msg_id": "9e762ae0f5da43e6bec5eb8143f1f834",
"from_offline": false
}
Untuk discriminant: 11:
- Lakukan deduplikasi berdasarkan
msg_id. Pesan yang sama dapat tiba secara langsung dan sekali lagi dari antrean luring. - Uraikan amplop dan pastikan
receiver_pubkeydi dalamnya adalah identitas Anda. - Untuk frame langsung (
from_offline: false), buang pesan jika stempel waktu amplop berselisih lebih dari 300 detik dari jam Anda. - Verifikasi tanda tangan amplop terhadap
sender_pubkeymilik amplop. Kunci tersebut, bukansender_pubkeymilik frame, yang merupakan pengirim terautentikasi. - Dekripsi dengan kunci chat yang diturunkan dari pengirim tersebut. Versi App yang sangat lama mengenkripsi dengan output X25519 mentah; coba kunci itu jika kunci HKDF gagal.
- Simpan pesan secara persisten, lalu kirim tanda terima pengiriman dan, untuk
from_offline: true, pengakuan luring.
Amplop 1:1 dikirimkan tanpa payload_sig; tanda tangan amplop berfungsi sebagai pemeriksaan keaslian. Frame juga dapat membawa contact_request: true.
Jika pesan tidak ditujukan kepada Anda, gagal diverifikasi, atau gagal didekripsi, buang pesan tersebut tanpa menampilkan apa pun.
Pengiriman luring
Setiap pesan, suntingan, penarikan, reaksi, tanda terima, dan tanda baca ditulis ke antrean luring penerima sebelum pengiriman langsung. Antrean diputar ulang secara otomatis setelah setiap login dan saat diminta:
{ "type": "relay_pull" }
Relay memutar ulang semua item dalam antrean sebagai jenis frame normalnya dengan from_offline: true, diurutkan berdasarkan stempel waktu, lalu diikuti oleh:
{ "type": "relay_pull_done", "count": 12, "has_more": false }
Pengiriman bersifat setidaknya satu kali (at-least-once). Item tetap berada di antrean hingga diakui:
{ "type": "relay_offline_ack", "msg_id": "9e762ae0f5da43e6bec5eb8143f1f834" }
Akui reaksi dengan "reaction_id" alih-alih "msg_id". Kirim pengakuan hanya setelah item tersimpan secara persisten di perangkat. Relay membalas dengan {"type":"relay_offline_ack","msg_id":"...","success":true}.
Batas antrean:
| Batas | Nilai |
|---|---|
| Item per penerima | 1.000. Jika penuh, item baru ditolak dan pengirim melihat durable: false. |
| Retensi | 72 jam setelah item terbaru ditambahkan. |
| Ukuran payload | 1 MiB setelah didekode, dalam batas frame 1 MiB. |
Relay adalah buffer pengiriman, bukan riwayat pesan. Simpan riwayat di perangkat.
Tanda terima pengiriman dan tanda baca
Tanda terima pengiriman
Kirim setelah menyimpan pesan:
{ "type": "message_receipt", "receiver_pubkey": "<original sender>", "msg_id": "...", "timestamp": 1780000010 }
Relay membalas message_receipt_ack dan mengirimkan {"type":"message_receipt","sender_pubkey":"...","msg_id":"...","timestamp":...} kepada pengirim asli.
Tanda baca
{ "type": "message_read", "receiver_pubkey": "<original sender>", "msg_id": "<latest read message>", "timestamp": 1780000200, "enabled": true }
Tanda baca berfungsi sebagai watermark: App menandai pesan yang dirujuk beserta setiap pesan keluar sebelumnya dalam percakapan sebagai telah dibaca. Kirim tanda baca hanya jika pengguna mengaktifkan fitur tanda baca.
Relay menerapkan aturan timbal balik saat pengiriman, saat pengiriman langsung, dan saat pemutaran ulang. Tanda baca hanya dikirimkan jika kedua pengguna saling menjadi kontak dan keduanya mengaktifkan read_receipts_enabled. Jika tidak, pengirim menerima:
{ "type": "message_read_ack", "msg_id": "...", "delivered": false, "suppressed": true, "reason": "receiver_read_receipts_disabled" }
| Alasan | Arti |
|---|---|
client_disabled | Frame membawa enabled: false atau read_receipts_enabled: false. |
not_mutual_contact | Kedua pengguna tidak saling menjadi kontak. |
reader_read_receipts_disabled | Pembaca menonaktifkan tanda baca. |
receiver_read_receipts_disabled | Pengirim asli menonaktifkan tanda baca. |
invalid_pubkey | Format kunci publik tidak valid. |
Tanda baca yang berhasil dikirimkan diakui dengan {"type":"message_read_ack","msg_id":"...","delivered":true}.
Suntingan dan penarikan
Suntingan
Suntingan adalah amplop tersegel baru (dengan message_id acaknya sendiri) yang berisi konten pengganti secara lengkap, dikirim dengan merujuk ID pesan asli:
{
"type": "message_edit",
"receiver_pubkey": "<64 hex>",
"msg_id": "<original msg_id>",
"target_msg_id": "<original msg_id>",
"payload_b64": "<base64 envelope>",
"payload_sig": "<128 hex>",
"timestamp": 1780000400
}
payload_sig menggunakan rumus di bagian Tandatangani frame dengan discriminant 11. Suntingan yang menghapus semua lampiran menetapkan "attachments_edited": true di dalam JSON plaintext-nya. Relay membalas message_edit_ack. Penerima memverifikasi dan mendekripsi amplop seperti pesan biasa, dan hanya boleh menerapkan suntingan jika pengirimnya adalah penulis pesan asli.
Penarikan
{
"type": "message_revoke",
"receiver_pubkey": "<64 hex>",
"sender_pubkey": "<64 hex>",
"msg_id": "<original msg_id>",
"timestamp": 1780000500,
"payload_sig": "<128 hex>"
}
Tanda tangan penarikan dibuat atas byte mentah, tanpa hashing:
payload_sig = Ed25519(identity_key,
"aeronyx-message-revoke-v1" || sender_pubkey[32] || receiver_pubkey[32]
|| UTF-8(msg_id) || timestamp as u64 little-endian)
Relay membalas message_revoke_ack. Relay tidak memeriksa kepengarangan: penerima harus memverifikasi tanda tangan dan hanya menerapkan penarikan jika pengirimnya adalah penulis pesan asli. Untuk menghapus lampiran dari pesan yang ditarik, panggil POST /api/relay/blob/{blob_id}/delete/.
Reaksi
Reaksi 1:1 adalah amplop tersegel dengan message_id berupa ID reaksi, content_type 2, dan plaintext {"emoji": "❤️", "op": "add"} (atau "remove"):
{
"type": "message_reaction",
"receiver_pubkey": "<64 hex>",
"msg_id": "<message being reacted to>",
"reaction_id": "<32 hex>",
"payload_b64": "<base64 envelope>",
"payload_sig": "<128 hex>",
"timestamp": 1780000300
}
payload_sigmenggunakan discriminant12.reaction_idadalah kunci idempotensi.reaction_idyang berulang dalam 72 jam diakui dengan"duplicate": truedan tidak dikirimkan lagi.- Relay membalas
{"type":"message_reaction_ack","msg_id":"...","reaction_id":"...","delivered":...,"queued":...}. - Reaksi dibatasi 20 per pengirim dan percakapan dalam jendela batas laju.
Reaksi grup dijelaskan di bagian Grup.
Status kehadiran dan indikator mengetik
Frame status kehadiran dan indikator mengetik merupakan metadata plaintext dan dapat dilihat oleh relay.
Status kehadiran
Berlangganan ke kontak:
{ "type": "presence_subscribe", "pubkeys": ["<64 hex>", "<64 hex>"] }
Maksimal 200 kunci diproses per frame; kunci dengan format tidak valid dan kunci duplikat diabaikan. Langganan terakumulasi selama masa hidup koneksi. Berlanggananlah hanya ke kontak Anda sendiri.
{
"type": "presence_subscribe_ack",
"count": 2,
"updates": [
{ "pubkey": "<a>", "visible": true, "presence_visible": true, "last_seen_visible": true, "online": false, "last_seen_ts": 1780000000, "reason": "allowed" },
{ "pubkey": "<b>", "visible": false, "presence_visible": false, "last_seen_visible": false, "reason": "not_mutual_contact" }
],
"server_ts": 1780000001
}
Status kehadiran hanya terlihat di antara pengguna yang saling menjadi kontak, dan hanya jika target mengaktifkan presence_enabled. Entri tersembunyi (reason not_mutual_contact atau presence_hidden) tidak berisi online maupun last_seen_ts. last_seen_ts hanya ada jika last_seen_visible bernilai true; jika tidak, tampilkan status umum seperti "terakhir dilihat baru-baru ini".
Perubahan secara langsung tiba sebagai:
{ "type": "presence_update", "pubkey": "<a>", "online": true, "presence_visible": true, "last_seen_visible": true, "last_seen_ts": 1780000100 }
Indikator mengetik
{ "type": "typing", "receiver_pubkey": "<64 hex>", "is_typing": true }
{ "type": "group_typing", "group_id": "<uuid>", "is_typing": true }
Indikator mengetik hanya diteruskan jika status kehadiran pengirim dapat dilihat oleh penerima (saling menjadi kontak dan presence_enabled), atau kepada anggota lain dari grup tempat pengirim bergabung. Indikator ini tidak pernah disimpan, dimasukkan ke antrean, atau dikirim sebagai push.
Profil dan pengaturan privasi
GET /api/relay/profile/
Authorization: Relay <pubkey>:<timestamp>:<signature>
{
"success": true,
"profile": {
"pubkey_hex": "<64 hex>",
"display_name": "AeroNyx User",
"bio": "",
"handle": "",
"avatar_url": "",
"status_text": "",
"privacy": { "presence_enabled": true, "last_seen_enabled": true, "read_receipts_enabled": true },
"updated_at": "2026-10-09T10:00:00Z"
}
}
Identitas tanpa profil menerima field kosong dan ketiga flag privasi bernilai true.
PATCH /api/relay/profile/
Authorization: Relay <pubkey>:<timestamp>:<signature>
Content-Type: application/json
{ "display_name": "Alice", "privacy": { "presence_enabled": true, "last_seen_enabled": false, "read_receipts_enabled": false } }
| Field | Aturan |
|---|---|
display_name | Maksimal 50 karakter. |
bio | Maksimal 200 karakter. |
avatar_url | URL https:// atau kosong. |
handle | a-z dan 0-9, 5–24 karakter. Handle tunduk pada aturan keanggotaan dan masa jeda perubahan. |
privacy.* | Boolean. Ketiga flag juga dapat dikirim di tingkat teratas. |
Error yang mungkin antara lain no_valid_fields, <flag>_invalid_boolean, handle_taken, dan handle_change_cooldown:<date>.
Jaga agar perilaku klien konsisten dengan pengaturan ini: jangan kirim tanda baca saat fitur tersebut nonaktif, dan jangan tampilkan status baca lawan bicara selama tanda baca Anda sendiri nonaktif.
Verifikasi kontak
Pengguna dapat mewajibkan orang asing melakukan verifikasi sebelum mengirim pesan. Jika penerima mengaktifkan fitur ini dan belum menambahkan pengirim sebagai kontak, relay_send ditolak dengan verification_required.
Untuk memulai percakapan, kirim satu pesan dengan "contact_request": true. Permintaan kontak melewati pemeriksaan ini dan dibatasi 3 per pasangan pengirim dan penerima dalam jendela bergulir 24 jam; permintaan berikutnya ditolak dengan contact_request_rate_limited. Flag contact_request diteruskan kepada penerima agar App dapat menampilkan pesan tersebut sebagai permintaan.
Verifikasi kontak berlaku untuk relay_send dan fallback HTTPS.
Grup
Pesan grup
Konten grup dienkripsi dengan kunci grup bersama berukuran 32 byte menggunakan AES-256-GCM:
payload_b64 = base64(nonce[12] || AES-256-GCM(group_key, plaintext_json) || tag[16])
JSON plaintext berisi text, type (text, media, system, atau reaction), sender_pubkey, created_at, serta secara opsional attachments, mentions, reply, forwarded, forwarded_from_name, forwarded_from_pubkey, dan link_preview.
{
"type": "group_send",
"msg_id": "<32 hex>",
"group_id": "<uuid>",
"payload_b64": "<base64>",
"timestamp": 1780000000,
"payload_sig": "<128 hex>",
"key_version": 3
}
payload_sig = Ed25519(identity_key,
SHA256(UTF-8(group_id) || sender_pubkey[32] || 0x19 || timestamp as u64 little-endian
|| decoded payload))
Relay memeriksa tanda tangan serta memastikan pengirim adalah anggota aktif, menyimpan pesan untuk setiap anggota aktif lainnya, lalu mengirimkannya secara langsung. Payload grup tidak menggunakan amplop 1:1, dan amplop grup dikirimkan bersama group_id, key_version, dan payload_sig agar penerima dapat memverifikasi pengirim sebelum mendekripsi.
{
"type": "group_delivered",
"msg_id": "...",
"group_id": "...",
"accepted": true,
"accepted_count": 5,
"delivered_count": 2,
"queued_count": 5,
"failed_count": 0,
"member_count": 6
}
Pengirim yang bukan anggota menerima error dengan alasan not_a_member.
Suntingan, penarikan, dan reaksi grup
| Frame | Tanda tangan |
|---|---|
group_message_edit (group_id, msg_id, target_msg_id, payload_b64, payload_sig, key_version, timestamp) | Rumus grup di atas. |
group_message_reaction (group_id, msg_id, reaction_id, payload_b64, payload_sig, key_version, timestamp) | Rumus grup di atas. Payload-nya berupa payload grup dengan type: "reaction". |
group_message_revoke (group_id, sender_pubkey, msg_id, timestamp, payload_sig) | Ed25519 mentah atas `"aeronyx-group-message-revoke-v1" |
Masing-masing diakui dengan frame _ack yang sesuai, yang memuat jumlah pengiriman. Penerima hanya menerapkan suntingan dan penarikan dari penulis aslinya.
Kunci grup
Kunci grup didistribusikan oleh pemilik grup dalam bentuk bundel kunci per anggota: base64(nonce[12] || AES-256-GCM(chat_key(owner, member), group_key) || tag[16]). Endpoint REST di bawah /api/relay/groups/ digunakan untuk membuat grup, mengelola anggota dan undangan, mengunggah bundel kunci, merotasi kunci (keys/rotate/), dan mengambil bundel terkini milik pemanggil (keys/me/). Pengirim mengenkripsi dengan versi kunci terbaru yang dimilikinya; penerima yang tidak memiliki suatu versi kunci harus mengambil keys/me/ dan menahan pesan hingga kunci tersebut tersedia.
Lampiran terenkripsi
Lampiran dienkripsi di perangkat, diunggah sebagai ciphertext yang tidak dapat dibaca, dan dirujuk dari dalam pesan terenkripsi.
Enkripsi file
Untuk setiap file, buat kunci acak 32 byte dan nonce acak 12 byte:
blob = nonce[12] || AES-256-GCM(file_key, file_bytes) || tag[16]
Unggah blob. Letakkan file_key dan setiap field deskriptif di dalam pesan terenkripsi, jangan pernah di dalam permintaan unggah.
Objek lampiran
| Kunci | Wajib | Arti |
|---|---|---|
blob_id | ya | ID yang dikembalikan oleh proses unggah. |
file_key | ya | Base64 dari kunci file 32 byte. |
media_type | ya | Tipe MIME file plaintext. |
file_name | ya | Nama tampilan. |
file_size | ya | Ukuran plaintext dalam byte. |
thumb_b64 | tidak | Thumbnail JPEG dalam Base64, maksimal 64 KiB. |
duration_ms | tidak | Durasi audio atau video. |
waveform | tidak | Hingga 96 angka dalam rentang [0, 1] untuk pesan suara. |
sticker, sticker_pack, sticker_pose | tidak | Identitas stiker. |
live | tidak | Bagian gerak Live Photo: {blob_id, file_key, media_type, file_size, duration_ms?, width?, height?}. |
Penerima mengabaikan lampiran yang tidak memiliki blob_id atau file_key. Pesan suara dari App menggunakan AAC-LC dalam kontainer MP4 (audio/mp4).
Unggah: permintaan tunggal (hingga 10 MiB)
POST /api/relay/blob/presign/
Authorization: Relay <pubkey>:<timestamp>:<signature>
Content-Type: application/json
{ "file_size": 482141, "media_type": "image/jpeg", "media_kind": "image", "ttl_days": 7 }
file_size adalah ukuran ciphertext. media_kind bernilai salah satu dari voice, image, video, file, avatar, other. ttl_days dibatasi pada rentang 1–30 (default 7).
{
"blob_id": "<uuid>",
"upload_url": "https://...",
"upload_method": "PUT",
"storage": "r2",
"expires_at": "2026-10-16T10:00:00Z",
"media_type": "image/jpeg",
"media_kind": "image",
"access_mode": "capability",
"ttl_days": 7,
"max_bytes": 10485760
}
Kemudian:
- Lakukan
PUTciphertext keupload_urldalam 15 menit, hanya dengan headerContent-Type. Jangan kirim headerAuthorizationke penyimpanan. - Kirim
POST /api/relay/blob/{blob_id}/complete/dengan RelayAuth. Relay mengonfirmasi objek dan mengembalikan{blob_id, file_size, expires_at, storage}.
Unggah: multipart (hingga 100 MiB)
POST /api/relay/blob/multipart/create/
{ "total_size": 52428800, "media_type": "video/mp4", "media_kind": "video", "ttl_days": 7 }
{
"blob_id": "<uuid>",
"upload_id": "...",
"part_size": 8388608,
"total_parts": 7,
"part_urls": ["https://...", "..."],
"storage": "r2",
"expires_at": "...",
"max_bytes": 104857600
}
part_size secara default bernilai 8 MiB dan dapat diminta antara 5 dan 16 MiB. URL bagian berlaku selama 60 menit. Lakukan PUT untuk setiap bagian ke URL masing-masing dan catat header respons ETag, lalu:
POST /api/relay/blob/multipart/complete/
{ "blob_id": "<uuid>", "upload_id": "...", "parts": [ { "part_number": 1, "etag": "\"...\"" } ] }
AeroNyx App menggunakan unggahan permintaan tunggal untuk ciphertext hingga 8 MiB dan multipart untuk ukuran di atasnya.
Unduh
GET /api/relay/blob/{blob_id}/
Relay menjawab 302 dengan Location yang mengarah ke jaringan pengiriman konten (CDN) serta header X-AeroNyx-Blob-Size, X-AeroNyx-Blob-Media-Type, dan X-AeroNyx-Blob-Storage. Ikuti pengalihan tersebut secara mandiri tanpa meneruskan header Authorization apa pun, lalu verifikasi dan dekripsi blob dengan file_key miliknya. Batasi unduhan pada ukuran yang diharapkan.
blob_id adalah kapabilitas bearer: siapa pun yang memegangnya dapat mengambil ciphertext, itulah sebabnya kunci hanya dikirim di dalam pesan terenkripsi. access_mode dan masa kedaluwarsa diberlakukan pada pengalihan relay. Jangan mengandalkan keduanya untuk menjaga kerahasiaan.
Menghapus lampiran
POST /api/relay/blob/{blob_id}/delete/
Hanya pengunggah yang dapat menghapus blob. Responsnya adalah {"blob_id": "...", "deleted": true}.
Error lampiran
Error menggunakan format {"success": false, "error": "<text>", "error_code": "<code>"}. Tentukan penanganan berdasarkan error_code.
| HTTP | error_code | Arti |
|---|---|---|
| 400 | blob_id_invalid | Format ID blob tidak valid. |
| 400 | blob_missing_file_size, blob_missing_total_size, blob_missing_parts, blob_bad_parts, blob_bad_part_count | Permintaan unggah tidak valid. |
| 400 | blob_not_r2, blob_multipart_complete_failed | Penyelesaian gagal; mulai unggahan baru. |
| 401 | auth_required | Blob memerlukan RelayAuth. |
| 403 | blob_not_uploader, download_forbidden | Pemanggil tidak diizinkan. |
| 404 | blob_not_found, blob_not_uploaded | Blob tidak dikenal, atau penyelesaian dipanggil sebelum unggahan selesai. |
| 410 | blob_expired | Blob telah kedaluwarsa. Minta pengirim untuk mengirim ulang. |
| 413 | blob_too_large | Melebihi batas; respons menyertakan max_bytes dan chunked_max_bytes. |
| 503 | blob_r2_unavailable | Penyimpanan untuk sementara tidak tersedia; coba ulang dengan backoff. |
Endpoint unggah lama
POST /api/relay/blob/ (form multipart, hingga 10 MiB) dan API sesi yang dapat dilanjutkan di bawah /api/relay/blob/session/ (hingga 100 MiB, potongan 64 KiB hingga 4 MiB, sesi berlaku 24 jam) tetap tersedia sebagai fallback. Klien baru sebaiknya menggunakan endpoint di atas.
Notifikasi push
Relay mengirim notifikasi Apple Push Notification service (APNs) untuk iOS dan macOS. Klien Android hanya menerima pesan melalui WebSocket.
Push dikirim untuk relay_send dan group_send jika penerima tidak memiliki koneksi latar depan yang aktif, pengirim tidak menetapkan suppress_push, dan penerima tidak membisukan percakapan tersebut. Suntingan, penarikan, reaksi, dan tanda terima tidak memicu push. Payload push tidak berisi ciphertext:
{
"aps": { "alert": { "title": "AeroNyx", "body": "New encrypted message" }, "sound": "default", "badge": 1, "content-available": 1 },
"kind": "p2p_message",
"sender_pubkey": "<64 hex>",
"message_id": "..."
}
kind bernilai p2p_message (dengan sender_pubkey) atau group_message (dengan group_id). Notifikasi panggilan menggunakan missed_call dan payload panggilan khusus. Saat menerima push, sambungkan koneksi dan jalankan relay_pull.
| Endpoint | Body |
|---|---|
POST /api/relay/push/register/ | token (64 hex), platform (ios atau macos), bundle_id, environment (production atau sandbox), serta token_type (alert atau voip) dan provider (apns) yang bersifat opsional. |
POST /api/relay/push/unregister/ | token, serta platform, token_type, provider yang bersifat opsional. |
POST /api/relay/push/mute/ | kind (p2p atau group), target (kunci publik atau ID grup), muted (boolean). |
Ketiganya memerlukan RelayAuth. Mendaftarkan token akan memindahkan token tersebut ke identitas pemanggil.
Panggilan
Pensinyalan panggilan suara dan video berjalan melalui WebSocket yang sama sebagai metadata plaintext; media mengalir secara terpisah. Frame yang digunakan adalah call_invite, call_answer, call_reject, call_hangup, dan call_busy untuk panggilan 1:1, serta group_call_invite, group_call_invite_broadcast, group_call_answer, dan group_call_hangup_broadcast untuk grup, ditambah frame penerimaan peserta untuk rapat yang dipandu host.
Relay memvalidasi room_name terhadap para peserta: p2p_ diikuti 16 karakter hex pertama dari SHA256(lower_key + ":" + higher_key) untuk panggilan 1:1, dan grp_<first 8 characters of group_id>_<first 8 hex of SHA256(group_id)> untuk grup. Sinyal panggilan untuk penerima yang sedang luring ditahan selama 120 detik.
Batas laju
| Cakupan | Batas | Respons |
|---|---|---|
relay_send, group_send, dan push HTTPS, per identitas | 50 per jendela pendek; 100.000 per hari | WebSocket: error rate_limited dengan retry_after. HTTPS: 429 dengan Retry-After. |
group_send per pengirim dan grup | 10 per jendela pendek | error rate_limited, scope: "sender". |
group_send per grup | 50 per jendela pendek | error rate_limited, scope: "group". |
| Reaksi per pengirim dan percakapan | 20 per jendela pendek | error rate_limited, scope: "reaction". |
| Permintaan kontak per pengirim dan penerima | 3 per 24 jam | send_rejected contact_request_rate_limited. |
Lakukan backoff setidaknya selama retry_after detik. Jangan mengirim ulang dalam loop yang rapat: penghitung tetap berjalan selama Anda mencoba ulang.
Daftar periksa implementasi
- Buat dan simpan identitas Ed25519; gunakan kunci hex huruf kecil di semua tempat.
- Implementasikan RelayAuth serta login, heartbeat, dan
presence_stateWebSocket. - Implementasikan amplop tersegel dan verifikasi terhadap vektor acuan di Central Chat HTTPS API v1.
- Kirim dengan
relay_send, anggapdurable: truesebagai terkirim, dan buat ulangtimestampsertapayload_sigpada setiap percobaan ulang. - Terima
relay_envelope: lakukan deduplikasi, verifikasi, dekripsi, dan penyimpanan, lalu kirimmessage_receiptdanrelay_offline_ack. - Jalankan
relay_pullsetelah login dan saat aplikasi dibangunkan oleh push; kirim pengakuan hanya setelah data tersimpan secara persisten. - Baca flag privasi profil dan patuhi flag tersebut untuk status kehadiran dan tanda baca.
- Enkripsi lampiran di perangkat dan unggah melalui
presignatau multipart; simpanfile_keydi dalam pesan terenkripsi. - Verifikasi kepengarangan sebelum menerapkan suntingan dan penarikan.
- Simpan pencarian dan riwayat pesan di perangkat. Relay tidak menyediakan pencarian konten dan bukan arsip.
Pertanyaan yang sering diajukan
Dapatkah AeroNyx Chat Relay membaca pesan saya?
Tidak. Pesan, suntingan, reaksi, pesan grup, dan lampiran dienkripsi dan ditandatangani di perangkat pengirim sebelum mencapai relay, dan kuncinya tidak pernah meninggalkan perangkat orang-orang yang terlibat dalam percakapan. Relay hanya menyimpan dan meneruskan ciphertext.
Apa yang dapat dilihat oleh AeroNyx Chat Relay?
Relay melihat metadata pengiriman: kunci publik pengirim dan penerima, ID grup dan ID pesan, stempel waktu, ukuran payload, status pengiriman dan status baca, sinyal kehadiran dan indikator mengetik, metadata pensinyalan panggilan, ukuran lampiran, serta alamat IP koneksi. Relay tidak melihat konten pesan, konten lampiran, maupun kunci. Daftar lengkapnya terdapat dalam model kepercayaan di bagian atas halaman ini.
Enkripsi apa yang digunakan oleh chat AeroNyx?
Setiap identitas adalah pasangan kunci Ed25519. Dua orang menurunkan kunci chat bersama dengan X25519 dan HKDF-SHA256. Pesan 1:1 dienkripsi dengan XChaCha20-Poly1305 dan ditandatangani dengan Ed25519. Pesan grup dan lampiran dienkripsi dengan AES-256-GCM. Format byte yang persis beserta vektor uji dipublikasikan dalam dokumentasi Central Chat HTTPS API v1.
Bagaimana foto, video, dan file dilindungi?
Setiap file dienkripsi di perangkat dengan kunci AES-256-GCM acaknya sendiri sebelum diunggah. Penyimpanan hanya menerima ciphertext. Kunci file dikirim di dalam pesan terenkripsi ujung ke ujung, sehingga hanya penerima yang dapat mendekripsi file tersebut.
Apa yang terjadi jika penerima sedang luring?
Relay menyimpan pesan terenkripsi dalam antrean luring penerima hingga 72 jam dan mengirimkannya ketika penerima terhubung kembali. Di iOS dan macOS, penerima juga menerima notifikasi push yang tidak berisi konten pesan.
Apakah AeroNyx menyimpan riwayat chat saya?
Tidak. Relay adalah buffer pengiriman: item dihapus setelah perangkat penerima mengonfirmasi bahwa item tersebut telah disimpan. Riwayat chat dan pencarian berada di perangkat Anda.
Dapatkah saya membangun klien atau bot AeroNyx sendiri?
Ya. Perangkat lunak apa pun yang memiliki identitas Ed25519 dan mengimplementasikan format di halaman ini dapat bertukar pesan dengan pengguna AeroNyx App. Untuk integrasi berbasis permintaan/respons yang lebih sederhana tanpa WebSocket, gunakan Central Chat HTTPS API v1.
Apakah AeroNyx Chat Relay terdesentralisasi?
Chat Relay adalah layanan pengiriman terpusat milik AeroNyx. AeroNyx juga mengoperasikan jaringan node sumber terbuka (AGPL-3.0) yang dapat membawa ciphertext chat melalui rute dua-hop yang melintasi jaringan-jaringan berbeda. Kedua jalur ini berdampingan: relay menyediakan pengiriman yang cepat dan andal serta antrean luring, sedangkan jalur node merupakan rute opsional yang mengurangi apa yang dapat diamati oleh satu operator mana pun.
Mengapa pesan saya ditolak dengan verification_required?
Penerima hanya menerima pesan dari kontak. Kirim satu pesan pertama dengan contact_request: true, yang akan dilihat penerima sebagai permintaan kontak. Maksimal tiga permintaan kontak per penerima diizinkan dalam setiap jangka waktu 24 jam.
Pengiriman dua-hop terverifikasi
Untuk lalu lintas ChatRelay terautentikasi yang memenuhi syarat, sumber dapat memilih jalur dua-hop dengan keragaman jaringan dan menghitung pengiriman sebagai berhasil hanya setelah memvalidasi tanda terima bertanda tangan dari terminal yang diharapkan. Node relay merutekan ciphertext dan tidak mengurai payload E2E. Lihat model bukti selengkapnya di Penemuan Node dan Pengiriman Relay Terenkripsi Terverifikasi.
<!-- verified-two-hop-delivery-v1:end -->