AeroNyx Chat Relay 客戶端整合

AeroNyx2026年6月19日4 分鐘閱讀33 次瀏覽

AeroNyx 盲中繼訊息、互為聯絡人 presence、對等 read receipt、加密 reaction、離線佇列及可續傳密文媒體的客戶端契約。

本頁是 App、前端、後端與 AI coding agent 實作 AeroNyx 相容聊天客戶端時使用的正式 frame 與 media API 契約。所有內容都遵守 relay 只路由密文、不能理解使用者內容的原則。

不可妥協的隱私不變量

Relay 不得解析、儲存或推導聊天明文、reaction 內容、語音/媒體明文、解密金鑰、nonce、waveform、檔名、轉錄、MemChain 明文、封包負載、DNS、目的地、URL、瀏覽紀錄、錢包層級流量或私有身分 seed。內容必須在離開客戶端前完成 E2E 加密。

payload_b64payload_sig 承載 E2E 內容。Relay 可見範圍只限 typemsg_idreaction_idreceiver_pubkeygroup_id、受限時間戳、投遞狀態、blob 大小/到期、access mode 與聚合計數器。

Presence 隱私不變量

只有互為非刪除聯絡人的兩個 P2P 身分,才能互相訂閱 online 與 last seen。後端會雙向檢查 P2PContact,因此攻擊者不能只憑公鑰掃描使用者是否在線。使用者也能分別關閉 presence 與精確 last seen。

隱藏結果不包含 onlinelast_seen_ts,只回傳 reason=not_mutual_contactreason=presence_hidden

Profile 隱私 API

客戶端應在連線後讀取 profile privacy flags,並讓本地 UI 與後端門控一致。PATCH 支援巢狀 privacy 物件,也保留三個頂層欄位以相容舊客戶端。

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

Presence frames

只對聯絡人清單送出 presence_subscribe。當 last_seen_enabled=false 時,客戶端可以顯示模糊的「最近上線」或完全隱藏時間,不應從其他訊號自行推測精確狀態。

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
}

Read receipt 對等規則

Read receipt 採對等規則:使用者關閉後,客戶端不發送自己的 message_read,也不顯示對方的已讀;若任一方關閉或雙方不是互為聯絡人,後端會 suppress frame。Read receipt 只包含中繼資料,不包含訊息內容。

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

可能的 suppression reason 包含 client_disablednot_mutual_contactreader_read_receipts_disabledreceiver_read_receipts_disabled;離線 pull 也套用同一門控。

Emoji reactions

Reaction 本身也是 E2E 密文。Relay 只按接收方或 group membership 路由事件,使用 reaction_id 去重,對方離線時進入同一套 store-and-forward 佇列,不維護某則訊息的 reaction 聚合狀態。

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 為 12reaction_id 同時是冪等與離線 ACK key,ACK 為 message_reaction_ack。群組使用 group_message_reactiongroup_idkey_version

加密媒體 blob 模型

語音、大圖、影片與檔案先在客戶端加密,再上傳 ciphertext。blob_id、解密金鑰、nonce、時長、waveform、顯示檔名與 preview metadata 必須放在 relay_send.payload_b64 內,不能作為 blob API 明文欄位。

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
}

簡單密文 blob 上傳

短語音與小圖可以使用單次 multipart 上傳。後端只接受密文字節;預設 TTL 為 7 天、政策範圍為 1 到 30 天,下載可以使用不可猜測 capability 或指定 P2P 公鑰的 authenticated 模式。

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

單次上限為 10 MB。超限回 HTTP 413error_code=blob_too_large,並提供 chunked_max_bytes=104857600

可續傳密文 blob 上傳

超過單次限制時使用分塊 session。總密文上限為 100 MB;同一 chunk index 可安全重試,客戶端應在本地保存 upload_idchunk_size 與已完成 index。

1. 建立 upload 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. 上傳分塊

http
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. 網絡中斷後續傳

http
GET /api/relay/blob/session/{upload_id}/
Authorization: Relay <pubkey>:<timestamp>:<signature>

讀取 missing_chunks 後只上傳缺失 index;upload session 有效 24 小時。

4. 完成上傳

http
POST /api/relay/blob/session/{upload_id}/complete/
Authorization: Relay <pubkey>:<timestamp>:<signature>

完成操作會核對所有分塊及總位元組;安全重試會返回既有 final blob,不會建立重複物件。

5. 取消上傳

http
DELETE /api/relay/blob/session/{upload_id}/
Authorization: Relay <pubkey>:<timestamp>:<signature>

下載密文 blob

Capability 模式不需要 RelayAuth,因為不可猜測的 UUID 本身就是 bearer capability;authenticated 模式必須簽名,且公鑰必須是 uploader 或 allowed_downloaders 成員。過期 blob 會在存取時懶清理。

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

客戶端 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。

節點發現與可驗證的加密中繼投遞