AeroNyx Chat Relay 客户端集成

AeroNyx2026年6月19日4 分钟阅读30 次浏览

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。

节点发现与可验证的加密中继投递