AeroNyx Chat Relay 客戶端整合
AeroNyx 盲中繼訊息、互為聯絡人 presence、對等 read receipt、加密 reaction、離線佇列及可續傳密文媒體的客戶端契約。
本頁是 App、前端、後端與 AI coding agent 實作 AeroNyx 相容聊天客戶端時使用的正式 frame 與 media API 契約。所有內容都遵守 relay 只路由密文、不能理解使用者內容的原則。
不可妥協的隱私不變量
Relay 不得解析、儲存或推導聊天明文、reaction 內容、語音/媒體明文、解密金鑰、nonce、waveform、檔名、轉錄、MemChain 明文、封包負載、DNS、目的地、URL、瀏覽紀錄、錢包層級流量或私有身分 seed。內容必須在離開客戶端前完成 E2E 加密。
payload_b64 與 payload_sig 承載 E2E 內容。Relay 可見範圍只限 type、msg_id、reaction_id、receiver_pubkey、group_id、受限時間戳、投遞狀態、blob 大小/到期、access mode 與聚合計數器。
Presence 隱私不變量
只有互為非刪除聯絡人的兩個 P2P 身分,才能互相訂閱 online 與 last seen。後端會雙向檢查 P2PContact,因此攻擊者不能只憑公鑰掃描使用者是否在線。使用者也能分別關閉 presence 與精確 last seen。
隱藏結果不包含 online 或 last_seen_ts,只回傳 reason=not_mutual_contact 或 reason=presence_hidden。
Profile 隱私 API
客戶端應在連線後讀取 profile privacy flags,並讓本地 UI 與後端門控一致。PATCH 支援巢狀 privacy 物件,也保留三個頂層欄位以相容舊客戶端。
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
}
}
Presence frames
只對聯絡人清單送出 presence_subscribe。當 last_seen_enabled=false 時,客戶端可以顯示模糊的「最近上線」或完全隱藏時間,不應從其他訊號自行推測精確狀態。
{
"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
}
Read receipt 對等規則
Read receipt 採對等規則:使用者關閉後,客戶端不發送自己的 message_read,也不顯示對方的已讀;若任一方關閉或雙方不是互為聯絡人,後端會 suppress frame。Read receipt 只包含中繼資料,不包含訊息內容。
{
"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"
}
可能的 suppression reason 包含 client_disabled、not_mutual_contact、reader_read_receipts_disabled、receiver_read_receipts_disabled;離線 pull 也套用同一門控。
Emoji reactions
Reaction 本身也是 E2E 密文。Relay 只按接收方或 group membership 路由事件,使用 reaction_id 去重,對方離線時進入同一套 store-and-forward 佇列,不維護某則訊息的 reaction 聚合狀態。
{
"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 同時是冪等與離線 ACK key,ACK 為 message_reaction_ack。群組使用 group_message_reaction、group_id 與 key_version。
加密媒體 blob 模型
語音、大圖、影片與檔案先在客戶端加密,再上傳 ciphertext。blob_id、解密金鑰、nonce、時長、waveform、顯示檔名與 preview metadata 必須放在 relay_send.payload_b64 內,不能作為 blob API 明文欄位。
{
"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
}
簡單密文 blob 上傳
短語音與小圖可以使用單次 multipart 上傳。後端只接受密文字節;預設 TTL 為 7 天、政策範圍為 1 到 30 天,下載可以使用不可猜測 capability 或指定 P2P 公鑰的 authenticated 模式。
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 |
單次上限為 10 MB。超限回 HTTP 413、error_code=blob_too_large,並提供 chunked_max_bytes=104857600。
可續傳密文 blob 上傳
超過單次限制時使用分塊 session。總密文上限為 100 MB;同一 chunk index 可安全重試,客戶端應在本地保存 upload_id、chunk_size 與已完成 index。
1. 建立 upload 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. 上傳分塊
PUT /api/relay/blob/session/{upload_id}/chunk/{chunk_index}/
Authorization: Relay <pubkey>:<timestamp>:<signature>
Content-Type: application/octet-stream
預設分塊 1 MB、最大 4 MB;重傳同一 index 會覆蓋舊分塊,因此是冪等操作。
3. 網絡中斷後續傳
GET /api/relay/blob/session/{upload_id}/
Authorization: Relay <pubkey>:<timestamp>:<signature>
讀取 missing_chunks 後只上傳缺失 index;upload session 有效 24 小時。
4. 完成上傳
POST /api/relay/blob/session/{upload_id}/complete/
Authorization: Relay <pubkey>:<timestamp>:<signature>
完成操作會核對所有分塊及總位元組;安全重試會返回既有 final blob,不會建立重複物件。
5. 取消上傳
DELETE /api/relay/blob/session/{upload_id}/
Authorization: Relay <pubkey>:<timestamp>:<signature>
下載密文 blob
Capability 模式不需要 RelayAuth,因為不可猜測的 UUID 本身就是 bearer capability;authenticated 模式必須簽名,且公鑰必須是 uploader 或 allowed_downloaders 成員。過期 blob 會在存取時懶清理。
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 |
客戶端 UX 指引
小檔優先單次上傳;超過 max_bytes 才切分塊。App 重啟後先查 missing_chunks,不要重傳已完成分塊。所有 media secret 留在 E2E payload;收到 410 blob_expired 時提供重新傳送,而不是顯示底層例外。
AI agent 整合順序
AI coding agent 應依序實作 RelayAuth、profile privacy、presence UI、read receipt 對等規則、reaction 冪等、單次密文 blob、分塊續傳,最後把 blob reference 放入加密 relay_send。不要建立伺服器端聊天全文搜尋,因為 relay 無法搜尋密文明文。
已驗證兩跳投遞
符合條件且已認證的 ChatRelay 流量可以選擇網絡多樣化兩跳路徑;來源只有在驗證預期 terminal 的簽名 receipt 後才計入投遞。中間節點只路由 ciphertext,不解析 E2E payload。