AeroNyx 聊天 API 整合指南

11 分鐘閱讀

透過 HTTPS 收發端對端加密的 AeroNyx 訊息:請求簽章、可直接執行的快速開始、已封裝的信封格式、錯誤碼與測試向量。

AeroNyx 聊天 API 讓伺服器、機器人或應用程式能透過一般的 HTTPS,與 AeroNyx 使用者收發端對端加密的一對一訊息。它不會使用 AeroNyx 隱私網路、VPN、洋蔥路由或匿名信箱。

傳送是即時的:收件者的 App 在線時,訊息會立刻送到。透過此 API 接收則是輪詢:GET /inbox/ 只回傳當下正在等待的內容,不會保持請求不斷開。若要即時接收,請讓同一個身分連線到聊天中繼 WebSocket(wss://api.aeronyx.network/ws/relay/),詳見下方「即時遞送」一節。

基礎網址:https://api.aeronyx.network/api/relay/developer/v1

運作方式

  1. 建立身分。 一組 Ed25519 金鑰對就是你的聊天身分。32 位元組的公鑰以 64 個小寫十六進位字元表示,即為你的聊天位址。不需要 AeroNyx 帳號。
  2. 為每個請求簽章。 每次呼叫都帶有以你的金鑰簽署的 Authorization 標頭(見驗證)。
  3. 封裝每則訊息。 在你這一端加密並簽署訊息,只傳送產生的信封(見訊息信封)。
  4. 傳送: 使用 POST /messages/。
  5. 接收: 使用 GET /inbox/,解密並儲存每則訊息,再以 POST /acks/ 確認。
端點驗證用途
GET /capabilities/無服務限制與支援的功能
POST /messages/必要傳送已封裝的訊息
GET /inbox/必要取得尚未確認的訊息
POST /acks/必要確認你已儲存的訊息

快速開始

這個腳本會從一個新身分傳送訊息給另一個新身分,讀回該訊息、解密並確認。請將下方的參考實作(Python)儲存為 aeronyx_chat.py,以 pip install pynacl requests 安裝相依套件,然後執行。

python
from __future__ import annotations

import hashlib, json, os, time, uuid

import requests
from nacl.signing import SigningKey

from aeronyx_chat import open_, seal

BASE = "https://api.aeronyx.network"
PREFIX = "/api/relay/developer/v1"


def call(key: SigningKey, method: str, path: str, body: dict | None = None):
    """Signs and sends one request. `path` is the full path after the host."""
    raw = b"" if body is None else json.dumps(body, separators=(",", ":")).encode()
    seconds = str(int(time.time()))
    nonce = os.urandom(16)
    digest = hashlib.sha256(
        b"AeroNyx/DeveloperChatRequestV1\0" + method.encode() + b"\0"
        + path.encode() + b"\0" + seconds.encode() + b"\0"
        + nonce + hashlib.sha256(raw).digest()
    ).digest()
    signature = key.sign(digest).signature.hex()
    pub = key.verify_key.encode().hex()
    headers = {
        "Authorization": f"AeroNyx-Chat-V1 {pub}:{seconds}:{nonce.hex()}:{signature}",
        "Content-Type": "application/json",
    }
    # Send exactly the bytes that were signed.
    return requests.request(method, BASE + path, data=raw or None,
                            headers=headers, timeout=20)


alice, bob = SigningKey.generate(), SigningKey.generate()
bob_address = bob.verify_key.encode().hex()

# 1. Alice seals and sends a message to Bob.
message_id = uuid.uuid4()
now = int(time.time())
sent = call(alice, "POST", PREFIX + "/messages/", {
    "receiver_pubkey": bob_address,
    "message_id": str(message_id),
    "discriminant": 11,
    "sent_at": now,
    "payload_b64": seal(alice, bob.verify_key.encode(), "Hello Bob", message_id, now),
})
print("send:", sent.status_code, sent.json())

# 2. Bob fetches his inbox, decrypts, and then acknowledges.
inbox = call(bob, "GET", PREFIX + "/inbox/?limit=50").json()
for item in inbox["messages"]:
    opened = open_(bob, item["payload_b64"])
    print("received:", opened["plaintext"], "from", opened["sender"][:8])
    # Store the message durably here, then acknowledge it.
    ack = call(bob, "POST", PREFIX + "/acks/", {"message_ids": [item["msg_id"]]})
    print("ack:", ack.status_code, ack.json())

驗證

除了 GET /capabilities/ 之外,每個端點都需要這個標頭:

text
Authorization: AeroNyx-Chat-V1 <public-key-hex>:<unix-seconds>:<nonce-hex>:<signature-hex>
  • public-key-hex 是你的 64 字元聊天位址。
  • unix-seconds 必須與伺服器時鐘相差在 60 秒以內。
  • nonce-hex 是 16 個隨機位元組,以 32 個小寫十六進位字元表示。每個 nonce 只能使用一次。
  • signature-hex 是對下列 32 位元組摘要所做的 Ed25519 簽章,以 128 個十六進位字元表示:
text
SHA256(
  "AeroNyx/DeveloperChatRequestV1\0" ||
  UPPERCASE_HTTP_METHOD || "\0" ||
  PATH_WITH_QUERY || "\0" ||
  ASCII_UNIX_SECONDS || "\0" ||
  NONCE_BYTES ||
  SHA256(EXACT_HTTP_BODY_BYTES)
)

PATH_WITH_QUERY 是實際送出的完整請求路徑,包含 /api/relay/developer/v1 前綴與任何查詢字串。 例如:

請求要簽署的 PATH_WITH_QUERY
POST https://api.aeronyx.network/api/relay/developer/v1/messages//api/relay/developer/v1/messages/
GET https://api.aeronyx.network/api/relay/developer/v1/inbox/?limit=50/api/relay/developer/v1/inbox/?limit=50

EXACT_HTTP_BODY_BYTES 是請求本文實際送出的位元組。因此 JSON 的空白字元與鍵的順序都很重要:只序列化一次,簽署這些位元組,並送出完全相同的位元組。沒有本文的請求,簽署的是空字串的 SHA-256。

驗證測試向量

在呼叫 API 之前,請用它檢查你的簽章程式碼。這把金鑰僅供測試;請勿用於真實訊息。

json
{
  "seed_hex": "0303030303030303030303030303030303030303030303030303030303030303",
  "public_key_hex": "ed4928c628d1c2c6eae90338905995612959273a5c63f93636c14614ac8737d1",
  "method": "POST",
  "path_with_query": "/api/relay/developer/v1/messages/",
  "unix_seconds": "1780000000",
  "nonce_hex": "000102030405060708090a0b0c0d0e0f",
  "body": "{\"receiver_pubkey\":\"8139770ea87d175f56a35466c34c7ecccb8d8a91b4ee37a25df60f5b8fc9b394\",\"message_id\":\"018f5164-c1b2-7e2b-9d8a-2c60597f4c2f\",\"discriminant\":11,\"sent_at\":1780000000,\"payload_b64\":\"AAAA\"}",
  "body_sha256_hex": "1971a2ef86b2a085e033fd65313981c8668204d49cedbe530875036d1f45c9cc",
  "digest_hex": "6f4d0ca6dcd1221780b2dae45dd94e4bae7e79f266e8bb69f2d50c20de08b276",
  "signature_hex": "14ed2108e1ecf8ce5d04299e59af6d00dec218d62592de127e6a640f235ea4e9703004561fc08e929d0159f92fc51d35a881eec7d0eb16f19d94270c6262cb0c",
  "authorization": "AeroNyx-Chat-V1 ed4928c628d1c2c6eae90338905995612959273a5c63f93636c14614ac8737d1:1780000000:000102030405060708090a0b0c0d0e0f:14ed2108e1ecf8ce5d04299e59af6d00dec218d62592de127e6a640f235ea4e9703004561fc08e929d0159f92fc51d35a881eec7d0eb16f19d94270c6262cb0c"
}

Ed25519 簽章具有決定性,因此相同的輸入必定得出完全一樣的 signature_hex。如果你的 body_sha256_hex 不同,就表示你的本文位元組不同。

排解 401

API 對所有驗證失敗都回應相同的 401,因此請依序檢查:

  1. 時鐘:unix-seconds 與實際時間相差在 60 秒以內。
  2. nonce 每個請求都是新的,長度為 16 位元組,並以 32 個小寫十六進位字元表示。
  3. 簽署的路徑包含 /api/relay/developer/v1 與查詢字串。
  4. 簽署的方法為大寫,且與請求相符。
  5. 簽署的本文位元組就是你送出的位元組。簽署後不要重新序列化 JSON。
  6. 你的摘要與簽章和上方的測試向量一致。

端點

服務資訊

GET /capabilities/ 不需要驗證,會回傳目前的限制與支援的功能。

傳送訊息

POST /messages/

json
{
  "receiver_pubkey": "<64 lowercase hex characters>",
  "message_id": "018f5164-c1b2-7e2b-9d8a-2c60597f4c2f",
  "discriminant": 11,
  "sent_at": 1780000000,
  "payload_b64": "<sealed envelope, Base64>"
}

五個欄位全部必填,且不允許其他欄位。

  • receiver_pubkey 是收件者的聊天位址,不得與你自己的位址相同。
  • message_id 是 UUID。每則訊息請使用新的隨機 UUID,並保留完全相同的本文以供重試。以相同的 message_id 但不同的欄位傳送,會回傳 409 idempotency_conflict。
  • discriminant 為 11,代表 AeroNyx App 使用者應該會看到的一對一訊息。
  • sent_at 是 Unix 秒數,必須與伺服器時間相差在 300 秒以內。
  • payload_b64 是訊息信封中所述的已封裝信封。

中繼只檢查 payload_b64 是否為 1 位元組至 191 KiB 的有效 Base64。它無法分辨信封與其他資料,因此不是有效信封的酬載在這裡會被接受,並由收件者的 App 靜默丟棄。請用黃金向量測試你的信封。

成功的請求會回傳 HTTP 202:

json
{
  "message_id": "018f5164-c1b2-7e2b-9d8a-2c60597f4c2f",
  "accepted": true,
  "delivered_live": false
}

accepted 表示中繼已將密文持久儲存。delivered_live 為 true 只表示中繼已把訊息交給即時遞送且沒有出錯;即使收件者沒有任何連線,它也是 true,所以不能用來判斷對方是否在線。兩者都不代表收件者已解密或顯示該訊息;收件匣裡的遞送回條才代表這件事(見「收件匣中的事件」)。

取得收件匣

GET /inbox/?limit=50

最多回傳 100 筆尚未確認的事件。同一個事件在逾時或重新啟動後可能再次出現。

json
{
  "messages": [{
    "sender_pubkey": "<hex>",
    "discriminant": 11,
    "payload_b64": "<ciphertext>",
    "timestamp": 1780000000,
    "msg_id": "018f5164-c1b2-7e2b-9d8a-2c60597f4c2f"
  }],
  "has_more": false
}

如果 has_more 為 true,請確認你已儲存的訊息,然後再次取得。

收件匣中的事件

收件匣存放中繼為你的身分排隊的所有項目,不只是新訊息。沒有 control_type 的項目是訊息:依「訊息信封」一節開啟 payload_b64。有 control_type 的項目是關於某則訊息的事件。解密任何內容前,請先檢查 control_type:回條沒有 payload_b64。

control_type意義msg_id
message_receipt收件者的 App 已解密並儲存你傳送的訊息。你那則訊息的 message_id
message_read收件者已讀你的訊息。只有對方開啟已讀回條時才會送出。你那則訊息的 message_id
message_edit傳送者編輯了一則訊息(target_msg_id,與 msg_id 相同)。被編輯訊息的 ID
message_revoke傳送者收回了一則訊息。被收回訊息的 ID
message_reaction對 target_msg_id 新增或移除了表情回應。表情回應的 ID

編輯、收回與表情回應都有簽章(payload_sig),格式見 AeroNyx 聊天中繼用戶端整合。確認一個 ID 會移除佇列中所有帶有該 ID 的項目,因此請處理完整頁再確認。你不處理的事件類型也請確認,否則會再次回傳。

確認訊息

POST /acks/

json
{
  "message_ids": ["018f5164-c1b2-7e2b-9d8a-2c60597f4c2f"]
}

只有在你驗證、解密訊息並寫入持久儲存空間之後,才可確認該訊息。未知的 ID 不會出現在回應的 acknowledged 清單中。

遞送模型

  • 遞送為至少一次。請以 control_type 加上 msg_id 去除重複:回條帶的是它所確認的那則訊息的 ID。
  • 未遞送的密文會保留 72 小時。請勿將此服務當作永久的歷史紀錄。
  • 傳送失敗時,請以完全相同的本文與全新的驗證 nonce 重試。

即時遞送

傳送

POST /messages/ 會把訊息交給收件者目前開著的每一條連線,同時把它留在佇列中,直到收件者的 App 確認為止。收件者的 App 在線時會立刻收到;離線的話,會在 App 下次連線時收到(72 小時內)。

透過此 API 傳送的訊息不會觸發推播通知。App 已關閉的收件者不會收到提醒,會在下次開啟 App 時看到訊息。

以輪詢接收

GET /inbox/ 會立即回傳,不論有沒有內容在等待。訊息要等你下一次呼叫才會拿到,所以你看到的延遲就是你的輪詢間隔。對話類用途請改用 WebSocket 接收。

即時接收

讓同一個身分連線到聊天中繼 WebSocket,不需要帳號或註冊。

  1. 開啟 wss://api.aeronyx.network/ws/relay/,並用同一把 Ed25519 金鑰登入,方法見 AeroNyx 聊天中繼用戶端整合 的「登入」一節。
  2. 登入後,中繼會先重送所有仍在佇列中的項目(from_offline 為 true),之後每個新項目一到就立刻送出。訊息以 relay_envelope 訊框送達,欄位與收件匣中的訊息相同;回條、編輯、收回與表情回應則以 type 等於上表 control_type 的訊框送達。
  3. 儲存項目後,確認它:
json
{"type": "relay_offline_ack", "msg_id": "018f5164-c1b2-7e2b-9d8a-2c60597f4c2f"}

WebSocket 與 GET /inbox/ 讀的是同一個佇列:在其中一邊確認過的項目,另一邊也不會再出現,POST /acks/ 對兩者都有效。你可以繼續用 HTTPS 傳送,同時用 WebSocket 接收。

限制與錯誤

限制數值
已封裝酬載191 KiB(Base64 的 JSON 請求會維持在請求上限以下)
請求本文256 KiB
收件匣分頁與確認批次100 筆
離線保留72 小時
狀態意義
400 invalid_json本文不是 JSON 物件。
400 invalid_request本文未恰好包含這五個欄位、receiver_pubkey 不是 64 個小寫十六進位字元或等於你自己的位址、discriminant 超出 1-255、message_id 不是 UUID,或 payload_b64 不是有效的 Base64。
400 invalid_payload解碼後的酬載為空,或大於 191 KiB。
400 message_expiredsent_at 與伺服器時間相差超過 300 秒。
401驗證缺失、已過期、無效或遭重放。請見排解 401。
403收件者的政策拒絕了這個寄件者。
409冪等性衝突,或相同的請求仍在處理中。
429達到傳送方頻率限制:每個身分每秒 50 個請求,每個 UTC 日 100,000 個請求。請等待 Retry-After 標頭指定的時間。
503持久遞送或驗證暫時無法使用。請以全新的 nonce 重試完全相同的本文。

本指南不涵蓋的內容

本指南只涵蓋透過 HTTPS 收發一對一訊息。其他功能請讓同一個身分連線到聊天中繼 WebSocket,並參閱 AeroNyx 聊天中繼用戶端整合:

  • 群組訊息、在線狀態與輸入中提示。
  • 加密附件,包括語音訊息與影片檔案。
  • 語音與視訊通話。該文章列出了信令幀,但完整的通話整合指南(媒體權杖、邀請簽章與通話加密)尚未發布。

隱私與可見性

訊息內容為端對端加密:它在你這一端以由寄件者與收件者身分衍生的金鑰封裝,AeroNyx 從不持有該金鑰,而 API 只儲存並轉送密文。為了遞送訊息,服務可以看到寄件者與收件者的公鑰、訊息時間、大小、遞送狀態,以及每個請求的 IP 位址。HTTPS 無法對服務隱藏這些中繼資料。

訊息信封

AeroNyx App 使用者應該會看到的訊息,使用 discriminant: 11,並在 payload_b64 中放入端對端封裝的信封。中繼從不會看到金鑰或明文。所有整數皆為小端序。

1. 共用金鑰

雙方都從各自的 Ed25519 聊天身分衍生出相同的 32 位元組金鑰:

text
my_x25519_secret = clamp(SHA-512(my_ed25519_seed)[0..32])
peer_x25519      = Edwards-to-Montgomery(peer_ed25519_public_key)
raw              = X25519(my_x25519_secret, peer_x25519)
key              = HKDF-SHA256(salt = empty, ikm = raw, info = "AERONYX-P2P-KEY", length = 32)

使用 libsodium 時,這是 crypto_sign_ed25519_sk_to_curve25519、crypto_sign_ed25519_pk_to_curve25519 與 crypto_scalarmult,之後再接 HKDF。寄件者一律使用 HKDF 金鑰。非常舊的 App 版本直接以 raw 傳送,因此若 HKDF 金鑰解密失敗,接收者可以嘗試 raw。

2. 明文

文字訊息就是其 UTF-8 文字。來自 App、帶有附件、回覆或轉寄的訊息,是含有 "type": "aeronyx_message" 與 text 欄位的 JSON 物件;如果你不支援其餘部分,請顯示 text。

3. 加密

使用 XChaCha20-Poly1305,搭配全新的隨機 24 位元組 nonce,且不使用附加資料。下方的 ciphertext 是包含 16 位元組標籤的密碼輸出。

4. 簽章

寄件者以 Ed25519 簽署這 121 個位元組:

text
sender_pubkey   32 bytes
message_id      16 bytes   (the raw bytes of the UUID sent as message_id)
receiver_pubkey 32 bytes
timestamp        8 bytes   (u64, Unix seconds; use the same value as sent_at)
content_type     1 byte    (0 = text)
SHA-256(ciphertext) 32 bytes

5. 信封

text
message_id      16 bytes
sender_pubkey   32 bytes
receiver_pubkey 32 bytes
timestamp        8 bytes   (u64)
ciphertext_len   8 bytes   (u64)
ciphertext       ciphertext_len bytes
nonce           24 bytes
content_type     4 bytes   (u32, 0 = text)
signature       64 bytes

payload_b64 是這些位元組的標準 Base64。請注意,content_type 在簽署資料中是 1 位元組,但在信封中是 4 位元組。

接收

解析信封,檢查 receiver_pubkey 是你的身分,用信封的 sender_pubkey 驗證簽章,然後用由該寄件者衍生的金鑰解密。任何一個步驟失敗就丟棄該訊息。App 會丟棄 timestamp 與其時鐘相差超過 300 秒的即時訊息。

參考實作(Python)

python
"""AeroNyx 1:1 chat envelope (discriminant 11). Requires PyNaCl."""

from __future__ import annotations

import base64
import hashlib
import hmac
import os
import struct
import uuid

from nacl.bindings import (
    crypto_aead_xchacha20poly1305_ietf_decrypt,
    crypto_aead_xchacha20poly1305_ietf_encrypt,
    crypto_scalarmult,
    crypto_sign_ed25519_pk_to_curve25519,
    crypto_sign_ed25519_sk_to_curve25519,
)
from nacl.signing import SigningKey, VerifyKey

CONTENT_TEXT = 0
_HKDF_INFO = b"AERONYX-P2P-KEY"


def _hkdf_sha256(ikm: bytes, info: bytes, length: int = 32) -> bytes:
    # RFC 5869 with an empty salt (= 32 zero bytes).
    prk = hmac.new(b"\x00" * 32, ikm, hashlib.sha256).digest()
    okm, block, counter = b"", b"", 1
    while len(okm) < length:
        block = hmac.new(prk, block + info + bytes([counter]), hashlib.sha256).digest()
        okm += block
        counter += 1
    return okm[:length]


def chat_key(my_key: SigningKey, peer_public: bytes, hkdf: bool = True) -> bytes:
    """X25519 over the Ed25519 identities, then HKDF-SHA256."""
    my_secret = bytes(my_key) + my_key.verify_key.encode()  # libsodium 64-byte sk
    raw = crypto_scalarmult(
        crypto_sign_ed25519_sk_to_curve25519(my_secret),
        crypto_sign_ed25519_pk_to_curve25519(peer_public),
    )
    return _hkdf_sha256(raw, _HKDF_INFO) if hkdf else raw


def _sign_data(sender, message_id, receiver, timestamp, content_type, ciphertext):
    return (
        sender
        + message_id
        + receiver
        + struct.pack("<Q", timestamp)
        + bytes([content_type])  # one byte here ...
        + hashlib.sha256(ciphertext).digest()
    )


def seal(my_key: SigningKey, receiver: bytes, text: str, message_id: uuid.UUID,
         timestamp: int, nonce: bytes | None = None) -> str:
    """Returns payload_b64 for POST /messages/ (discriminant 11)."""
    nonce = nonce if nonce is not None else os.urandom(24)
    key = chat_key(my_key, receiver)
    ciphertext = crypto_aead_xchacha20poly1305_ietf_encrypt(
        text.encode("utf-8"), None, nonce, key)  # ciphertext || 16-byte tag, no AAD
    sender = my_key.verify_key.encode()
    mid = message_id.bytes
    signature = my_key.sign(
        _sign_data(sender, mid, receiver, timestamp, CONTENT_TEXT, ciphertext)
    ).signature
    envelope = (
        mid
        + sender
        + receiver
        + struct.pack("<Q", timestamp)
        + struct.pack("<Q", len(ciphertext))
        + ciphertext
        + nonce
        + struct.pack("<I", CONTENT_TEXT)  # ... four bytes here
        + signature
    )
    return base64.b64encode(envelope).decode("ascii")


def open_(my_key: SigningKey, payload_b64: str) -> dict:
    """Verifies and decrypts an inbox payload addressed to my_key."""
    env = base64.b64decode(payload_b64, validate=True)
    if len(env) < 16 + 32 + 32 + 8 + 8 + 16 + 24 + 4 + 64:
        raise ValueError("envelope too short")
    mid, sender, receiver = env[0:16], env[16:48], env[48:80]
    timestamp, ct_len = struct.unpack("<QQ", env[80:96])
    if len(env) != 96 + ct_len + 24 + 4 + 64:
        raise ValueError("envelope length mismatch")
    ciphertext = env[96:96 + ct_len]
    nonce = env[96 + ct_len:120 + ct_len]
    (content_type,) = struct.unpack("<I", env[120 + ct_len:124 + ct_len])
    signature = env[124 + ct_len:]
    if receiver != my_key.verify_key.encode():
        raise ValueError("envelope is addressed to another identity")
    VerifyKey(sender).verify(
        _sign_data(sender, mid, receiver, timestamp, content_type, ciphertext), signature)
    # Current App versions use the HKDF key; very old ones sent with the raw key.
    for hkdf in (True, False):
        try:
            plaintext = crypto_aead_xchacha20poly1305_ietf_decrypt(
                ciphertext, None, nonce, chat_key(my_key, sender, hkdf))
            break
        except Exception:  # noqa: BLE001 - try the legacy key next
            plaintext = None
    if plaintext is None:
        raise ValueError("decryption failed")
    return {
        "message_id": str(uuid.UUID(bytes=mid)),
        "sender": sender.hex(),
        "timestamp": timestamp,
        "content_type": content_type,
        "plaintext": plaintext.decode("utf-8"),
    }

黃金向量

固定的金鑰、nonce、ID 與時間,僅供測試。請勿使用這些金鑰。

json
{
  "sender_seed_hex": "0101010101010101010101010101010101010101010101010101010101010101",
  "sender_pubkey": "8a88e3dd7409f195fd52db2d3cba5d72ca6709bf1d94121bf3748801b40f6f5c",
  "receiver_seed_hex": "0202020202020202020202020202020202020202020202020202020202020202",
  "receiver_pubkey": "8139770ea87d175f56a35466c34c7ecccb8d8a91b4ee37a25df60f5b8fc9b394",
  "message_id": "018f5164-c1b2-7e2b-9d8a-2c60597f4c2f",
  "timestamp": 1780000000,
  "nonce_hex": "000102030405060708090a0b0c0d0e0f1011121314151617",
  "plaintext": "Hello from AeroNyx",
  "shared_key_hex": "dd08f8bb45a43c9bc186fdaf436d47e8e05b0aca44fa2bb9473769da0407652f",
  "payload_b64": "AY9RZMGyfiudiixgWX9ML4qI4910CfGV/VLbLTy6XXLKZwm/HZQSG/N0iAG0D29cgTl3Dqh9F19Wo1Rmw0x+zMuNipG07jeiXfYPW4/Js5QApRhqAAAAACIAAAAAAAAA1vVyzdTx1kAdPEI7AGO12ppwLwaME0K9PJKd+u7OgWq0rwABAgMEBQYHCAkKCwwNDg8QERITFBUWFwAAAACAlpInhB5A9cD4LqKAT0OE0OpbMqT5w/xB8IiHrzuyg9LPjF9gDHdOekPNoHNxDMeu3Ift0UB69OGSofdfwKIN"
}

seal(alice, bob_public, "Hello from AeroNyx", message_id, 1780000000, nonce=bytes(range(24))) 必須產生與此完全相同的 payload_b64,而 open_(bob, payload_b64) 必須回傳明文。此向量已對照 AeroNyx App 本身的金鑰協商、簽署器與密碼,以及節點的嚴格信封解碼器驗證過。

<!-- faq:start -->

常見問題

什麼是 AeroNyx 聊天 API?

它是具版本的 HTTPS API,可讓伺服器、機器人或應用程式與 AeroNyx 使用者交換端對端加密的一對一訊息。它有四個端點:capabilities、傳送、收件匣與確認。它只承載已封裝的信封;內容在你這一端加密。

使用 API 需要 AeroNyx 帳號嗎?

不需要。Ed25519 金鑰對就是身分。在本機產生它,並用它為每個請求簽章;公鑰就是你的聊天位址。

我應該簽署 /messages/ 還是完整路徑?

簽署實際送出的完整路徑,從 /api/relay/developer/v1 開始,若有查詢字串則一併包含。只簽署 /messages/ 會產生 401。本頁的驗證測試向量列出了確切的輸入。

為什麼我會收到 401?

所有驗證失敗都會回傳相同的 401。常見原因有:時鐘誤差超過 60 秒、重複使用 nonce、簽署的路徑或本文與實際送出的不同,或簽署資料中的 HTTP 方法為小寫。請逐項檢查排解 401 下的清單,並將你的摘要與測試向量比對。

為什麼 API 回傳 202,但訊息從未出現在 AeroNyx App 中?

中繼會接受任何有效的 Base64 酬載,但 App 只會顯示它能驗證並解密的已封裝信封。請將你的 payload_b64 與本頁的黃金向量比對,確認信封的 receiver_pubkey 是收件者,並讓信封的時間戳與目前時間相差在 300 秒以內。傳送以 Base64 編碼的純文字會被接受,但永遠不會顯示。

為什麼 API 回傳 invalid_request?

最常見的原因是傳送給你自己的公鑰、本文未恰好包含文件所列的五個欄位、message_id 不是 UUID,或 discriminant 超出 1-255。請見限制與錯誤下的錯誤表。

AeroNyx 能讀取透過此 API 傳送的訊息嗎?

不能。信封使用由寄件者與收件者身分衍生的金鑰加密,AeroNyx 從不持有該金鑰。服務可以看到遞送中繼資料,例如公鑰、時間、大小與 IP 位址。

未遞送的訊息會保留多久?

最多 72 小時。只有在你解密並儲存每則訊息之後才確認它;未確認的訊息可能會再次遞送。

我可以使用哪些程式語言?

任何支援 Ed25519、X25519、HKDF-SHA256 與 XChaCha20-Poly1305 的語言,其中包括大多數語言的 libsodium 繫結。本頁包含完整的 Python 參考實作與可執行的快速開始。

AeroNyx 聊天 API 是即時的嗎?

傳送是即時的:收件者的 App 在線時會立刻收到。透過 HTTPS 接收是輪詢,要等你下次呼叫 GET /inbox/ 才會看到新訊息。若要即時接收,請讓同一個身分連線到聊天中繼 WebSocket(wss://api.aeronyx.network/ws/relay/),它讀的是同一個佇列。

我傳的訊息會讓 AeroNyx 使用者收到通知嗎?

目前不會。透過此 API 傳送的訊息不會觸發推播通知,所以 App 已關閉的使用者會在下次開啟 App 時才看到;App 在線的使用者則會立刻看到。

<!-- faq:end -->