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。