AeroNyx Chat Relay クライアント統合
AeroNyx Chat Relay の統合リファレンスです。WebSocket 認証、封印済みの 1:1 メッセージとグループメッセージ、オフライン配信、受信確認、リアクション、オンライン状態、暗号化添付ファイル、プッシュ通知、レート制限について説明します。
連携用プロンプトを作成
技術スタックと作業を選び、コーディングアシスタントにコピーできます。端末内で生成し、AIサービスには送信しません。
生成されたプロンプト
本ページは AeroNyx Chat Relay の統合リファレンスです。AeroNyx Chat Relay は api.aeronyx.network で提供される WebSocket および HTTPS サービスであり、AeroNyx アイデンティティ間でエンドツーエンド暗号化された 1:1 メッセージ、グループメッセージ、受信確認、リアクション、オンライン状態、暗号化添付ファイルを中継します。
本ページは、AeroNyx 互換のクライアント、ボット、サービスを開発するエンジニア、およびそれらを実装する AI コーディングエージェントを対象としています。本ページに記載されているすべてのフレーム、フィールド、制限値は、2026 年 10 月時点の本番リレーおよび AeroNyx App の仕様を反映しています。
Chat Relay は中央集権型の配信経路です。分散型のノード経路(オニオンルーティングと匿名メールボックス。検証済み 2 ホップ配信を参照)と併存しており、クライアントは両方を併用できます。WebSocket を使用しないリクエスト/レスポンス型の HTTPS 統合については、Central Chat HTTPS API v1 を参照してください。
信頼モデル
リレーはコンテンツを一切認識できません。クライアントはリレーに届く前にすべてを暗号化・署名し、リレーは中身を読み取れない暗号文のルーティング、キューイング、レート制限のみを行います。
リレーが受け取ることのない情報:
- 平文のメッセージ本文、リアクションの絵文字、編集内容、グループペイロード
- チャット鍵、グループ鍵、添付ファイル鍵、ノンス
- 添付ファイルの内容、ファイル名、サムネイル、波形、文字起こし
- アイデンティティの秘密鍵
一方、リレーは配信メタデータを観測します。統合者は、これらがオペレーターから可視であるものとして扱ってください:
- 送信者と受信者の公開鍵、グループ ID、メッセージ ID
- タイムスタンプ、ペイロードサイズ、配信・受信確認・既読の状態
- オンライン状態、フォアグラウンド状態、入力中インジケーター(平文フレームとして送信)
- 通話シグナリングのメタデータ(ルーム名、通話 ID、ビデオフラグ)
- 添付ファイルの暗号文サイズ、申告されたメディアタイプ、有効期限
contact_requestフラグと IP レベルの接続メタデータ
コンテンツの機密性は、リレー上のアクセス制御ではなく、エンドツーエンド暗号化によって担保されます。これを前提に設計してください。保存された暗号文を入手した攻撃者であっても、それを読み取れない状態でなければなりません。
アイデンティティと鍵
AeroNyx のチャットアイデンティティは Ed25519 鍵ペアです。32 バイトの公開鍵を 64 文字の小文字 16 進数で表記したものがアドレスとなります。鍵はデバイス上で生成し、秘密鍵は決してどこにも送信しないでください。
アイデンティティの鍵ペアからは、次の 2 つが導出されます:
- チャット鍵(1:1)。
HKDF-SHA256(salt = empty, ikm = X25519(my_secret, peer_public), info = "AERONYX-P2P-KEY", length = 32)。ここで、両方の Ed25519 鍵は X25519 に変換されます(秘密鍵はSHA-512(seed)[0..32]をクランプしたもの、公開鍵は Edwards 形式から Montgomery 形式への変換)。両ピアは同一の鍵を導出します。 - メッセージ署名。 アイデンティティ鍵による Ed25519 署名であり、本ページで定義する正確なバイト列に対して行います。
フレーム内の公開鍵には常に小文字の 16 進数を使用してください。リレーはすべてのキューキーで大文字・小文字を正規化するわけではありません。
認証
RelayAuth 署名
WebSocket のログインと、認証が必要なすべての HTTPS エンドポイントでは、同じ署名を使用します:
digest = SHA256("AeroNyx-RelayAuth-v1" || pubkey[32] || timestamp as u64 little-endian)
signature = Ed25519(identity_key, digest)
"AeroNyx-RelayAuth-v1" は終端文字を含まない 20 バイトの ASCII です。timestamp は Unix 秒であり、サーバー時刻との差が 300 秒以内でなければなりません。
この署名が束縛するのはアイデンティティと時刻のみです。メソッド、パス、ボディ、接続は束縛せず、ノンスもありません。リクエストごとに新しいタイムスタンプを生成し、TLS 経由でのみ送信し、決してログに記録しないでください。
HTTPS ヘッダー
Authorization: Relay <pubkey-hex>:<timestamp>:<signature-hex>
検証に失敗すると、HTTP 401 と {"success": false, "error": "<reason>"} が返されます。理由には missing_auth_header、malformed_auth_header、invalid_timestamp、timestamp_expired、invalid_pubkey、invalid_signature があります。
WebSocket 接続
エンドポイント
wss://api.aeronyx.network/ws/relay/
ネイティブクライアントは Origin ヘッダーなしで接続します。ブラウザーは許可されたオリジンから接続する必要があります。それ以外のオリジン、または不明な Host からの接続は、コード 1008 で切断されます。
ログイン
サーバーはソケットを受け入れた後、30 秒以内に auth フレームが送信されることを想定しています:
{
"type": "auth",
"pubkey": "<64 hex>",
"timestamp": 1780000000,
"signature": "<128 hex>"
}
成功時:
{ "type": "auth_ack", "session_id": "<uuid>", "server_ts": 1780000001 }
server_ts を使用して時刻のずれを推定してください。メッセージのタイムスタンプも同じ ±300 秒の範囲で検証されます。
auth_ack の後、サーバーはハートビートを開始し、接続を対応するチャネルに登録して、直ちにオフラインキューを再送します(オフライン配信を参照)。
失敗時は {"type": "auth_error", "reason": "<reason>"} が送信され、ソケットはコード 4001 で切断されます。理由:missing_fields、timestamp_expired、invalid_pubkey、invalid_signature_length、invalid_signature_encoding、invalid_signature、internal_error。ログインがタイムアウトした場合は理由 timeout が送信され、コード 4002 で切断されます。
ログイン前に送信されたその他のフレームには、理由 authentication_required の auth_error が返されます。ソケットは開いたままです。
ハートビートとフォアグラウンド状態
| 方向 | フレーム | 動作 |
|---|---|---|
| サーバー → クライアント | {"type":"ping"} を 30 秒ごとに送信 | {"type":"pong"} を返します。 |
| クライアント → サーバー | {"type":"ping"} | サーバーが {"type":"pong"} を返します。 |
| クライアント → サーバー | {"type":"presence_state","foreground":true} | この接続をアクティブとしてマークします。 |
| クライアント → サーバー | {"type":"presence_state","foreground":false} | この接続をバックグラウンドとしてマークし、リレーが新着メッセージのプッシュ通知を送信できるようにします。 |
リレーは、接続が少なくとも 90 秒ごとに ping、pong、または foreground: true を指定した presence_state を送信している間のみ、そのアイデンティティをオンラインとみなします。AeroNyx App はフォアグラウンドにある間 15 秒ごとに ping を送信し、pong の欠落が 3 回を超えた場合は接続が切断されたものとして扱います。
長時間維持される接続は、少なくとも 24 時間に 1 回は再接続してください。接続が気付かないうちにライブフレームを受信しなくなっても、すべてのメッセージはキューにも保存され、ログイン時に再送されるため、メッセージが失われることはありません。ただし、ライブ配信が再開されるのは再接続後のみです。
フレームの規則
- テキストフレームのみを使用し、1 フレームにつき 1 つの JSON オブジェクトを送信します。バイナリフレームは無視されます。
- フレームの最大サイズは 1,048,576 文字です。これを超えるフレームは
{"type":"error","reason":"message_too_large"}で拒否されます。フレームの余地を残すため、payload_b64は約 800 KiB 未満に抑えてください。 - 不正な形式のフレームには、理由
invalid_json、invalid_json_type、またはunknown_typeのerrorが返されます。
汎用エラーフレーム:
{ "type": "error", "reason": "<reason>", "retry_after": 0 }
レート制限エラーには scope も含まれます。
クローズコード
| コード | 意味 |
|---|---|
1008 | Host または Origin が許可されていません。 |
4000 | サーバーがハートビートを送信できませんでした。 |
4001 | ログインに失敗しました。 |
4002 | ログインがタイムアウトしました。 |
指数バックオフとジッターを用いて再接続してください。AeroNyx App は、2^(attempt-1) 秒(160 秒の範囲に制限)に 0.81.2 のランダムな係数を掛けた時間だけ待機します。
1:1 メッセージの送信
1. 封印済みエンベロープの構築
1:1 メッセージのペイロードは、署名・暗号化された ChatEnvelope です。その正確な構築方法、Python リファレンス実装、ゴールデンテストベクターは Central Chat HTTPS API v1:封印済みエンベロープ形式 に記載されています。両方の API で同じエンベロープを使用します。
要約すると、チャット鍵による XChaCha20-Poly1305 暗号化、121 バイトのトランスクリプトに対する Ed25519 署名、および固定のバイナリレイアウトで構成されます。content_type は、添付ファイル付きのメッセージを含むすべてのメッセージで 0 です。
平文は、通常のメッセージでは UTF-8 テキスト、添付ファイル、返信、転送、リンクプレビューを含むメッセージでは JSON オブジェクトです:
{
"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": "..." }
}
type と text 以外のフィールドはすべて任意です。受信側は、"type": "aeronyx_message" を持つ JSON オブジェクトではない平文を、プレーンテキストとして表示してください。添付ファイルオブジェクトは「暗号化添付ファイル」で定義されています。
2. フレームへの署名
すべてのメッセージフレームには 2 つ目の署名 payload_sig が付与され、リレーはメッセージを受け入れる前にこれを検証します:
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 はデコードされた payload_b64 です。メッセージと編集には discriminant 11 を、リアクションには 12 を使用します。payload_sig は 16 進数でエンコードします。
3. 送信
{
"type": "relay_send",
"msg_id": "9e762ae0f5da43e6bec5eb8143f1f834",
"receiver_pubkey": "<64 hex>",
"discriminant": 11,
"payload_b64": "<base64 envelope>",
"timestamp": 1780000000,
"payload_sig": "<128 hex>"
}
| フィールド | 規則 |
|---|---|
msg_id | 小文字 16 進数 32 文字。エンベロープの 16 バイトの message_id です。受信側の App はこの ID でメッセージを保存するため、エンベロープと一致している必要があります。 |
receiver_pubkey | 受信者の公開鍵。 |
discriminant | 11。 |
timestamp | Unix 秒。サーバー時刻との差が ±300 秒以内。 |
suppress_push | 任意。true の場合、プッシュ通知を送信しません。 |
contact_request | 任意。連絡先認証を必須としている相手への最初のメッセージであることを示します(連絡先認証を参照)。 |
4. 確認応答
{ "type": "relay_delivered", "msg_id": "...", "delivered": true, "queued": true, "durable": true }
durable(およびqueued)は、メッセージが受信者のオフラインキューに保存されるとtrueになります。durable: trueを「送信済み」として扱ってください。deliveredは、受信者がアクティブなフォアグラウンド接続を持っていたことを意味します。受信の証明ではありません。受信の確認には配信確認を使用してください。
送信の試行を失敗とみなす前に、確認応答を最大 15 秒間待機してください。
拒否とエラー
{ "type": "send_rejected", "msg_id": "...", "reason": "verification_required" }
| レスポンス | 理由 | 対応 |
|---|---|---|
send_rejected | verification_required | 受信者は連絡先からのメッセージのみを受け付けています。再試行しないでください。 |
send_rejected | contact_request_rate_limited | この受信者に対する連絡先リクエストの上限に達しました。再試行しないでください。 |
error | missing_fields | 必須フィールドが存在しないか、空です。 |
error | invalid_timestamp、timestamp_expired | 時刻を修正し、タイムスタンプを付け直してください。 |
error | invalid_payload_sig | payload_sig の検証に失敗しました。 |
error | rate_limited | retry_after 秒後に再試行してください。 |
再試行
確認応答のないメッセージは、同じ msg_id と同じエンベロープのバイト列で再試行してください。リレーは 300 秒より古いフレームを拒否するため、再試行のたびにフレームの timestamp と payload_sig を新たに計算してください。エンベロープは元のタイムスタンプを保持します。リレーと受信側は msg_id によって重複を排除します。
HTTPS フォールバック
WebSocket が利用できない場合は、同じメッセージを 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>"
}
成功時は HTTP 200 と {"success": true} が返されます。連絡先認証に失敗した場合は 400 と verification_required または contact_request_rate_limited が、レート制限に達した場合は 429 と Retry-After が返されます。この方法で送信したメッセージは受信者のキューに格納されますが、プッシュ通知はトリガーされません。そのため、WebSocket が再接続されたら WebSocket 経由で再送してください。
メッセージの受信
受信メッセージは relay_envelope として届きます:
{
"type": "relay_envelope",
"sender_pubkey": "<64 hex>",
"discriminant": 11,
"payload_b64": "<base64 envelope>",
"timestamp": 1780000000,
"msg_id": "9e762ae0f5da43e6bec5eb8143f1f834",
"from_offline": false
}
discriminant: 11 の場合:
msg_idによって重複を排除します。同じメッセージがライブで届き、さらにオフラインキューから再度届くことがあります。- エンベロープを解析し、その
receiver_pubkeyが自分のアイデンティティであることを確認します。 - ライブフレーム(
from_offline: false)の場合、エンベロープのタイムスタンプが自分の時刻から 300 秒を超えてずれていれば、メッセージを破棄します。 - エンベロープの
sender_pubkeyに対してエンベロープの署名を検証します。認証された送信者は、フレームのsender_pubkeyではなく、この鍵です。 - その送信者から導出したチャット鍵で復号します。非常に古いバージョンの App は X25519 の生の出力で暗号化していました。HKDF 鍵で失敗した場合は、そちらも試してください。
- メッセージを永続的に保存してから配信確認を送信し、
from_offline: trueの場合はオフライン確認応答も送信します。
1:1 エンベロープは payload_sig なしで配信されます。真正性の確認はエンベロープの署名によって行います。フレームには contact_request: true が含まれる場合もあります。
自分宛てでないメッセージ、検証に失敗したメッセージ、復号に失敗したメッセージは、何も表示せずに破棄してください。
オフライン配信
すべてのメッセージ、編集、取り消し、リアクション、配信確認、既読通知は、ライブ配信の前に受信者のオフラインキューに書き込まれます。キューは各ログイン後に自動的に再送されるほか、リクエストに応じても再送されます:
{ "type": "relay_pull" }
リレーは、キュー内のすべての項目を通常のフレームタイプのまま from_offline: true を付けてタイムスタンプ順に再送し、その後に次のフレームを送信します:
{ "type": "relay_pull_done", "count": 12, "has_more": false }
配信は at-least-once(少なくとも 1 回)です。項目は確認応答されるまでキューに残ります:
{ "type": "relay_offline_ack", "msg_id": "9e762ae0f5da43e6bec5eb8143f1f834" }
リアクションの確認応答には、"msg_id" の代わりに "reaction_id" を使用してください。確認応答は、項目がデバイスに永続的に保存された後にのみ行ってください。リレーは {"type":"relay_offline_ack","msg_id":"...","success":true} を返します。
キューの制限:
| 制限 | 値 |
|---|---|
| 受信者あたりの項目数 | 1,000。満杯になると新しい項目は拒否され、送信者には durable: false が返されます。 |
| 保持期間 | 最後に項目が追加されてから 72 時間。 |
| ペイロードサイズ | デコード後 1 MiB(1 MiB のフレーム上限内)。 |
リレーは配信用のバッファであり、メッセージ履歴ではありません。履歴はデバイス上に保持してください。
配信確認と既読通知
配信確認
メッセージを保存した後に送信します:
{ "type": "message_receipt", "receiver_pubkey": "<original sender>", "msg_id": "...", "timestamp": 1780000010 }
リレーは message_receipt_ack を返し、元の送信者に {"type":"message_receipt","sender_pubkey":"...","msg_id":"...","timestamp":...} を配信します。
既読通知
{ "type": "message_read", "receiver_pubkey": "<original sender>", "msg_id": "<latest read message>", "timestamp": 1780000200, "enabled": true }
既読通知はウォーターマーク方式です。App は、指定されたメッセージと、その会話内でそれ以前に送信したすべてのメッセージを既読としてマークします。ユーザーが既読通知を有効にしている場合にのみ送信してください。
リレーは、送信時、ライブ配信時、再送時に相互ルールを適用します。既読通知は、両ユーザーが互いに連絡先であり、かつ両者が read_receipts_enabled を有効にしている場合にのみ配信されます。それ以外の場合、送信者は次のフレームを受け取ります:
{ "type": "message_read_ack", "msg_id": "...", "delivered": false, "suppressed": true, "reason": "receiver_read_receipts_disabled" }
| 理由 | 意味 |
|---|---|
client_disabled | フレームに enabled: false または read_receipts_enabled: false が含まれていました。 |
not_mutual_contact | ユーザー同士が互いに連絡先ではありません。 |
reader_read_receipts_disabled | メッセージを読んだ側が既読通知をオフにしています。 |
receiver_read_receipts_disabled | 元の送信者が既読通知をオフにしています。 |
invalid_pubkey | 公開鍵の形式が不正です。 |
配信された既読通知には、{"type":"message_read_ack","msg_id":"...","delivered":true} で確認応答が返されます。
編集と取り消し
編集
編集は、置き換え後の内容全体を含む新しい封印済みエンベロープ(独自のランダムな message_id を持つ)であり、元のメッセージ ID を対象として送信されます:
{
"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 は、「フレームへの署名」の式を discriminant 11 で使用します。すべての添付ファイルを削除する編集では、平文 JSON に "attachments_edited": true を設定します。リレーは message_edit_ack を返します。受信側はエンベロープを通常のメッセージと同様に検証・復号し、編集の送信者が元のメッセージの作成者である場合にのみ編集を適用しなければなりません。
取り消し
{
"type": "message_revoke",
"receiver_pubkey": "<64 hex>",
"sender_pubkey": "<64 hex>",
"msg_id": "<original msg_id>",
"timestamp": 1780000500,
"payload_sig": "<128 hex>"
}
取り消しの署名は、ハッシュ化せずに生のバイト列に対して行います:
payload_sig = Ed25519(identity_key,
"aeronyx-message-revoke-v1" || sender_pubkey[32] || receiver_pubkey[32]
|| UTF-8(msg_id) || timestamp as u64 little-endian)
リレーは message_revoke_ack を返します。リレーは作成者を検証しません。受信側は署名を検証し、取り消しの送信者が元のメッセージの作成者である場合にのみ取り消しを適用しなければなりません。取り消されたメッセージの添付ファイルを削除するには、POST /api/relay/blob/{blob_id}/delete/ を呼び出してください。
リアクション
1:1 リアクションは、message_id がリアクション ID である封印済みエンベロープであり、content_type は 2、平文は {"emoji": "❤️", "op": "add"}(または "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_sigには discriminant12を使用します。reaction_idは冪等性キーです。72 時間以内に同じreaction_idが繰り返された場合、"duplicate": trueで確認応答され、再配信はされません。- リレーは
{"type":"message_reaction_ack","msg_id":"...","reaction_id":"...","delivered":...,"queued":...}を返します。 - リアクションは、レート制限のウィンドウ内で送信者および会話ごとに 20 件までに制限されます。
グループのリアクションについては「グループ」で説明しています。
オンライン状態と入力中表示
オンライン状態と入力中表示のフレームは平文のメタデータであり、リレーから可視です。
オンライン状態
連絡先を購読します:
{ "type": "presence_subscribe", "pubkeys": ["<64 hex>", "<64 hex>"] }
1 フレームにつき最大 200 個の鍵が処理され、不正な形式の鍵や重複する鍵は無視されます。購読は接続が維持されている間、累積されます。自分の連絡先のみを購読してください。
{
"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
}
オンライン状態は、互いに連絡先であるユーザー間でのみ、かつ対象が presence_enabled を有効にしている場合にのみ可視となります。非表示のエントリ(reason が not_mutual_contact または presence_hidden)には、online や last_seen_ts は含まれません。last_seen_ts は last_seen_visible が true の場合にのみ含まれます。それ以外の場合は、「最近オンライン」などの汎用的な状態を表示してください。
ライブの変更は次の形式で届きます:
{ "type": "presence_update", "pubkey": "<a>", "online": true, "presence_visible": true, "last_seen_visible": true, "last_seen_ts": 1780000100 }
入力中表示
{ "type": "typing", "receiver_pubkey": "<64 hex>", "is_typing": true }
{ "type": "group_typing", "group_id": "<uuid>", "is_typing": true }
入力中表示は、送信者のオンライン状態が受信者から可視である場合(互いに連絡先であり、かつ presence_enabled が有効)、または送信者が所属するグループの他のメンバーに対してのみ転送されます。保存、キューイング、プッシュされることは一切ありません。
プロフィールとプライバシー設定
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"
}
}
プロフィールを持たないアイデンティティには、空のフィールドと、3 つのプライバシーフラグがすべて 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 } }
| フィールド | 規則 |
|---|---|
display_name | 最大 50 文字。 |
bio | 最大 200 文字。 |
avatar_url | https:// の URL または空。 |
handle | a-z と 0-9、5~24 文字。ハンドルにはメンバーシップのルールと変更のクールダウンが適用されます。 |
privacy.* | ブール値。3 つのフラグはトップレベルで送信することもできます。 |
エラーには no_valid_fields、<flag>_invalid_boolean、handle_taken、handle_change_cooldown:<date> があります。
クライアントはこれらの設定との整合性を保ってください。既読通知がオフの場合は既読通知を送信せず、自分の既読通知がオフの間は相手の既読状態を表示しないでください。
連絡先認証
ユーザーは、見知らぬ相手に対して、メッセージを送信する前に認証を求めることができます。受信者がこの設定を有効にしており、かつ送信者を連絡先に追加していない場合、relay_send は verification_required で拒否されます。
会話を開始するには、"contact_request": true を付けてメッセージを 1 件送信します。連絡先リクエストはこのチェックを回避でき、送信者と受信者の組み合わせごとに、直近 24 時間のローリングウィンドウ内で 3 件までに制限されます。それ以上のリクエストは contact_request_rate_limited で拒否されます。contact_request フラグは受信者に配信されるため、App はそのメッセージをリクエストとして表示できます。
連絡先認証は、relay_send と HTTPS フォールバックに適用されます。
グループ
グループメッセージ
グループのコンテンツは、共有の 32 バイトのグループ鍵を用いて AES-256-GCM で暗号化されます:
payload_b64 = base64(nonce[12] || AES-256-GCM(group_key, plaintext_json) || tag[16])
平文 JSON には text、type(text、media、system、または reaction)、sender_pubkey、created_at が含まれ、任意で attachments、mentions、reply、forwarded、forwarded_from_name、forwarded_from_pubkey、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))
リレーは署名と、送信者がアクティブなメンバーであることを確認し、他のすべてのアクティブなメンバー向けにメッセージを保存してから、ライブ配信します。グループペイロードは 1:1 エンベロープを使用しません。グループのエンベロープは group_id、key_version、payload_sig とともに配信されるため、受信側は復号前に送信者を検証できます。
{
"type": "group_delivered",
"msg_id": "...",
"group_id": "...",
"accepted": true,
"accepted_count": 5,
"delivered_count": 2,
"queued_count": 5,
"failed_count": 0,
"member_count": 6
}
メンバーではない送信者には、理由 not_a_member の error が返されます。
グループの編集、取り消し、リアクション
| フレーム | 署名 |
|---|---|
group_message_edit (group_id, msg_id, target_msg_id, payload_b64, payload_sig, key_version, timestamp) | 上記のグループ用の式。 |
group_message_reaction (group_id, msg_id, reaction_id, payload_b64, payload_sig, key_version, timestamp) | 上記のグループ用の式。ペイロードは type: "reaction" を持つグループペイロードです。 |
group_message_revoke (group_id, sender_pubkey, msg_id, timestamp, payload_sig) | `"aeronyx-group-message-revoke-v1" |
それぞれ、配信数を含む対応する _ack フレームで確認応答されます。受信側は、元の作成者からの編集と取り消しのみを適用します。
グループ鍵
グループ鍵は、グループのオーナーによってメンバーごとの鍵バンドル base64(nonce[12] || AES-256-GCM(chat_key(owner, member), group_key) || tag[16]) として配布されます。/api/relay/groups/ 配下の REST エンドポイントでは、グループの作成、メンバーと招待の管理、鍵バンドルのアップロード、鍵のローテーション(keys/rotate/)、呼び出し元の現在のバンドルの取得(keys/me/)を行います。送信者は保有している最新の鍵バージョンで暗号化します。該当する鍵バージョンを持たない受信者は keys/me/ を取得し、鍵が届くまでメッセージを保留してください。
暗号化添付ファイル
添付ファイルはデバイス上で暗号化され、中身を読み取れない暗号文としてアップロードされ、暗号化されたメッセージの内部から参照されます。
ファイルの暗号化
各ファイルについて、ランダムな 32 バイトの鍵とランダムな 12 バイトのノンスを生成します:
blob = nonce[12] || AES-256-GCM(file_key, file_bytes) || tag[16]
blob をアップロードします。file_key とすべての説明用フィールドは暗号化されたメッセージ内に格納し、アップロードリクエストには決して含めないでください。
添付ファイルオブジェクト
| キー | 必須 | 意味 |
|---|---|---|
blob_id | はい | アップロード時に返される ID。 |
file_key | はい | 32 バイトのファイル鍵の Base64。 |
media_type | はい | 平文ファイルの MIME タイプ。 |
file_name | はい | 表示名。 |
file_size | はい | 平文のサイズ(バイト単位)。 |
thumb_b64 | いいえ | Base64 の JPEG サムネイル(最大 64 KiB)。 |
duration_ms | いいえ | 音声または動画の再生時間。 |
waveform | いいえ | ボイスメッセージ用の [0, 1] の範囲の数値(最大 96 個)。 |
sticker, sticker_pack, sticker_pose | いいえ | スタンプの識別情報。 |
live | いいえ | Live Photo の動画部分:{blob_id, file_key, media_type, file_size, duration_ms?, width?, height?}。 |
blob_id または file_key を持たない添付ファイルは、受信側で無視されます。App から送信されるボイスメッセージは、MP4 コンテナに格納された AAC-LC(audio/mp4)です。
アップロード:単一リクエスト(最大 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 は暗号文のサイズです。media_kind は voice、image、video、file、avatar、other のいずれかです。ttl_days は 1~30 の範囲に制限されます(デフォルトは 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
}
その後、次の手順を実行します:
- 15 分以内に、
Content-Typeヘッダーのみを付けて暗号文をupload_urlにPUTします。ストレージにはAuthorizationヘッダーを送信しないでください。 - RelayAuth を付けて
POST /api/relay/blob/{blob_id}/complete/を送信します。リレーはオブジェクトを確認し、{blob_id, file_size, expires_at, storage}を返します。
アップロード:マルチパート(最大 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 のデフォルトは 8 MiB で、5~16 MiB の範囲で指定できます。パート URL の有効期間は 60 分です。各パートをそれぞれの URL に PUT し、ETag レスポンスヘッダーを記録してから、次を送信します:
POST /api/relay/blob/multipart/complete/
{ "blob_id": "<uuid>", "upload_id": "...", "parts": [ { "part_number": 1, "etag": "\"...\"" } ] }
AeroNyx App は、暗号文が 8 MiB 以下の場合は単一リクエストのアップロードを、それを超える場合はマルチパートを使用します。
ダウンロード
GET /api/relay/blob/{blob_id}/
リレーは、コンテンツ配信ネットワーク上の Location と、X-AeroNyx-Blob-Size、X-AeroNyx-Blob-Media-Type、X-AeroNyx-Blob-Storage ヘッダーを付けて 302 を返します。Authorization ヘッダーを転送せずに自身でリダイレクトをたどり、その後 file_key を使って blob を検証・復号してください。ダウンロードは想定サイズを上限としてください。
blob_id はベアラー型のケイパビリティです。これを保持している者は誰でも暗号文を取得できます。そのため、鍵は暗号化されたメッセージ内でのみ伝送されます。access_mode と有効期限はリレーのリダイレクト時に適用されますが、機密性の確保をこれらに依存しないでください。
添付ファイルの削除
POST /api/relay/blob/{blob_id}/delete/
blob を削除できるのは、アップロードした本人のみです。レスポンスは {"blob_id": "...", "deleted": true} です。
添付ファイルのエラー
エラーは {"success": false, "error": "<text>", "error_code": "<code>"} の形式を使用します。error_code で分岐してください。
| HTTP | error_code | 意味 |
|---|---|---|
| 400 | blob_id_invalid | blob ID の形式が不正です。 |
| 400 | blob_missing_file_size, blob_missing_total_size, blob_missing_parts, blob_bad_parts, blob_bad_part_count | アップロードリクエストが不正です。 |
| 400 | blob_not_r2, blob_multipart_complete_failed | 完了処理に失敗しました。新しいアップロードを開始してください。 |
| 401 | auth_required | この blob には RelayAuth が必要です。 |
| 403 | blob_not_uploader, download_forbidden | 呼び出し元には権限がありません。 |
| 404 | blob_not_found, blob_not_uploaded | 不明な blob であるか、アップロード完了前に完了処理が呼び出されました。 |
| 410 | blob_expired | blob の有効期限が切れています。送信者に再送を依頼してください。 |
| 413 | blob_too_large | 上限を超えています。レスポンスには max_bytes と chunked_max_bytes が含まれます。 |
| 503 | blob_r2_unavailable | ストレージが一時的に利用できません。バックオフしながら再試行してください。 |
レガシーのアップロードエンドポイント
POST /api/relay/blob/(マルチパートフォーム、最大 10 MiB)と、/api/relay/blob/session/ 配下の再開可能なセッション API(最大 100 MiB、チャンクサイズは 64 KiB~4 MiB、セッションの有効期間は 24 時間)は、フォールバックとして引き続き利用できます。新しいクライアントは上記のエンドポイントを使用してください。
プッシュ通知
リレーは、iOS と macOS 向けに Apple Push Notification service(APNs)の通知を送信します。Android クライアントは WebSocket 経由でのみメッセージを受信します。
relay_send と group_send のプッシュ通知は、受信者にアクティブなフォアグラウンド接続がなく、送信者が suppress_push を設定しておらず、かつ受信者が会話をミュートしていない場合に送信されます。編集、取り消し、リアクション、各種確認通知はプッシュされません。プッシュのペイロードには暗号文は含まれません:
{
"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 は p2p_message(sender_pubkey 付き)または group_message(group_id 付き)です。通話の通知には、missed_call と専用の通話ペイロードが使用されます。プッシュを受信したら、接続して relay_pull を実行してください。
| エンドポイント | ボディ |
|---|---|
POST /api/relay/push/register/ | token(64 文字の 16 進数)、platform(ios または macos)、bundle_id、environment(production または sandbox)、任意で token_type(alert または voip)と provider(apns)。 |
POST /api/relay/push/unregister/ | token、任意で platform、token_type、provider。 |
POST /api/relay/push/mute/ | kind(p2p または group)、target(公開鍵またはグループ ID)、muted(ブール値)。 |
3 つのエンドポイントはすべて RelayAuth を必要とします。トークンを登録すると、そのトークンは呼び出し元のアイデンティティに紐付け直されます。
通話
音声通話とビデオ通話のシグナリングは、平文のメタデータとして同じ WebSocket 上で伝送され、メディアは別経路で送受信されます。フレームは、1:1 通話では call_invite、call_answer、call_reject、call_hangup、call_busy、グループでは group_call_invite、group_call_invite_broadcast、group_call_answer、group_call_hangup_broadcast であり、ホスト付きミーティングには参加承認用のフレームがあります。
リレーは room_name を参加者と照合して検証します。1:1 通話では p2p_ に続けて SHA256(lower_key + ":" + higher_key) の先頭 16 文字の 16 進数、グループでは grp_<first 8 characters of group_id>_<first 8 hex of SHA256(group_id)> となります。オフラインの受信者宛ての通話シグナルは 120 秒間保持されます。
レート制限
| スコープ | 制限 | レスポンス |
|---|---|---|
relay_send、group_send、HTTPS プッシュ(アイデンティティごと) | 短期ウィンドウあたり 50 件、1 日あたり 100,000 件 | WebSocket:retry_after 付きの error rate_limited。HTTPS:Retry-After 付きの 429。 |
送信者およびグループごとの group_send | 短期ウィンドウあたり 10 件 | error rate_limited、scope: "sender"。 |
グループごとの group_send | 短期ウィンドウあたり 50 件 | error rate_limited、scope: "group"。 |
| 送信者および会話ごとのリアクション | 短期ウィンドウあたり 20 件 | error rate_limited、scope: "reaction"。 |
| 送信者および受信者ごとの連絡先リクエスト | 24 時間あたり 3 件 | send_rejected contact_request_rate_limited。 |
少なくとも retry_after 秒間はバックオフしてください。短い間隔のループで再送しないでください。再試行中もカウンターは加算され続けます。
実装チェックリスト
- Ed25519 アイデンティティを生成して保存し、すべての箇所で小文字 16 進数の鍵を使用します。
- RelayAuth と、WebSocket のログイン、ハートビート、
presence_stateを実装します。 - 封印済みエンベロープを実装し、Central Chat HTTPS API v1 のゴールデンベクターで検証します。
relay_sendで送信し、durable: trueを送信済みとして扱い、再試行時にはtimestampとpayload_sigを付け直します。relay_envelopeを受信したら、重複排除、検証、復号、保存を行い、その後message_receiptとrelay_offline_ackを送信します。- ログイン後とプッシュによる起動時に
relay_pullを実行し、永続的に保存した後にのみ確認応答します。 - プロフィールのプライバシーフラグを読み取り、オンライン状態と既読通知についてそれに従います。
- 添付ファイルはデバイス上で暗号化し、
presignまたはマルチパートでアップロードします。file_keyは暗号化されたメッセージ内に保持します。 - 編集と取り消しを適用する前に、作成者を検証します。
- メッセージの検索と履歴はデバイス上に保持します。リレーにはコンテンツ検索機能がなく、アーカイブでもありません。
よくある質問
AeroNyx Chat Relay はメッセージを読めますか?
いいえ。メッセージ、編集、リアクション、グループメッセージ、添付ファイルは、リレーに届く前に送信者のデバイス上で暗号化・署名され、鍵が会話参加者のデバイスの外に出ることはありません。リレーが保存・転送するのは暗号文だけです。
AeroNyx Chat Relay からは何が見えますか?
リレーから見えるのは配信メタデータです。具体的には、送信者と受信者の公開鍵、グループ ID とメッセージ ID、タイムスタンプ、ペイロードサイズ、配信状態と既読状態、プレゼンスと入力中シグナル、通話シグナリングのメタデータ、添付ファイルのサイズ、接続元 IP アドレスです。メッセージの内容、添付ファイルの内容、鍵は見えません。完全な一覧は、このページ冒頭の信頼モデルに記載しています。
AeroNyx のチャットはどの暗号方式を使っていますか?
各アイデンティティは Ed25519 鍵ペアです。2 人のユーザーは X25519 と HKDF-SHA256 で共有チャット鍵を導出します。1 対 1 のメッセージは XChaCha20-Poly1305 で暗号化され、Ed25519 で署名されます。グループメッセージと添付ファイルは AES-256-GCM で暗号化されます。正確なバイト形式とテストベクターは、Central Chat HTTPS API v1 のドキュメントで公開しています。
写真、動画、ファイルはどのように保護されますか?
各ファイルは、アップロード前にデバイス上で、ファイルごとに生成されるランダムな AES-256-GCM 鍵で暗号化されます。ストレージが受け取るのは暗号文だけです。ファイル鍵はエンドツーエンド暗号化されたメッセージの中で送られるため、ファイルを復号できるのは受信者だけです。
受信者がオフラインの場合はどうなりますか?
リレーは暗号化されたメッセージを受信者のオフラインキューに最大 72 時間保持し、受信者が再接続した時点で配信します。iOS と macOS では、メッセージ内容を含まないプッシュ通知も受信者に届きます。
AeroNyx はチャット履歴を保存しますか?
いいえ。リレーは配信用のバッファであり、受信者のデバイスが保存完了を確認した時点で項目は削除されます。チャット履歴と検索はお使いのデバイス上にあります。
独自の AeroNyx クライアントやボットを作れますか?
はい。Ed25519 アイデンティティを保持し、このページの形式を実装したソフトウェアであれば、どれでも AeroNyx App のユーザーとメッセージをやり取りできます。WebSocket を使わない、よりシンプルなリクエスト/レスポンス型の連携には、Central Chat HTTPS API v1 を使用してください。
AeroNyx Chat Relay は分散型ですか?
Chat Relay は AeroNyx の中央集権型配信サービスです。AeroNyx は、ネットワーク的に多様な 2 ホップ経路でチャットの暗号文を運べるオープンソース(AGPL-3.0)のノードネットワークも運用しています。2 つの経路は共存しています。リレーは高速で信頼性の高い配信とオフラインキューを提供し、ノード経路は、単一の運営者が観測できる情報を減らすための任意の経路です。
メッセージが verification_required で拒否されるのはなぜですか?
受信者は連絡先からのメッセージしか受け付けないためです。contact_request: true を付けた最初のメッセージを 1 通だけ送信してください。受信者にはこれが連絡先リクエストとして表示されます。連絡先リクエストは、任意の 24 時間のうちに受信者 1 人あたり 3 件まで送信できます。
検証済み 2 ホップ配信
対象となる認証済みの ChatRelay トラフィックについて、送信元はネットワーク的に分散した 2 ホップ経路を選択でき、想定される終端ノードの署名付き受領証を検証した後にのみ配信完了とみなします。リレーノードは暗号文をルーティングするだけで、E2E ペイロードを解析しません。完全な証拠モデルについては、ノード探索と検証済み暗号化リレー配信 を参照してください。
<!-- verified-two-hop-delivery-v1:end -->