{
  "name": "DVeProto",
  "version": 1.1,
  "handshake_version": 1,
  "canonical_url": "https://api.f-chat.ru/DVeProto",
  "documentation": {
    "human": "./",
    "note": "Human docs ship with this folder; drop on any domain."
  },
  "description": "Application-layer framing: X25519 ECDH, HKDF-SHA256, AES-256-GCM. Handshake is JSON (v1-compatible). After handshake, wire may be JSON text frames (1.0) or compact binary WebSocket frames (1.1).",
  "primitives": {
    "ecdh": "X25519",
    "hkdf": "HKDF-SHA256",
    "hkdf_salt": "",
    "hkdf_info_client_to_server": "DVeProto-v1/c2s",
    "hkdf_info_server_to_client": "DVeProto-v1/s2c",
    "aead": "AES-256-GCM",
    "nonce_length_bytes": 12
  },
  "wire_negotiation": {
    "hello_field": "offer",
    "ack_field": "select",
    "values": ["1.0", "1.1"],
    "default_if_absent": "1.0",
    "note": "dve_hello may include offer:[\"1.0\",\"1.1\"]. Client replies with select:\"1.1\" when supported. Old clients ignore offer and omit select → wire 1.0. Crypto handshake field v stays 1."
  },
  "transport": {
    "principle": "Handshake messages are UTF-8 JSON objects. After select=1.1, application data uses binary WebSocket frames only (no JSON/Base64/text). With select=1.0 (or legacy), data uses JSON text frames as in v1.",
    "examples": {
      "websocket_handshake": "Text frames: dve_hello then dve_client_ack.",
      "websocket_data_1_0": "One JSON string per text frame: type dve with base64 n/c.",
      "websocket_data_1_1": "One binary frame per message: ver|ptype|nonce|ciphertext||tag.",
      "https": "Handshake can stay JSON over POST bodies; binary 1.1 is defined for WebSocket. For HTTPS keep 1.0 JSON frames unless you negotiate an application-specific binary body.",
      "other": "Any mechanism that delivers handshake JSON; for 1.1 prefer a binary-capable channel with message boundaries (WSS binary, length-prefixed streams)."
    }
  },
  "handshake": {
    "order": {
      "server_sends": {
        "type": "dve_hello",
        "fields": ["proto", "v", "server_pk", "offer?"],
        "server_pk_encoding": "base64",
        "server_pk_bytes": 32,
        "offer": ["1.0", "1.1"]
      },
      "client_sends": {
        "type": "dve_client_ack",
        "fields": ["proto", "v", "client_pk", "select?"],
        "client_pk_encoding": "base64",
        "client_pk_bytes": 32,
        "select": "1.0|1.1"
      }
    },
    "note": "Server uses a fresh X25519 key pair per logical session. Shared secret = X25519(server_private, client_public). HKDF labels remain DVeProto-v1/c2s and DVeProto-v1/s2c for both wire 1.0 and 1.1."
  },
  "data_frame_1_0": {
    "type": "dve",
    "wire": "1.0",
    "fields": {
      "proto": "DVeProto",
      "v": 1,
      "n": "12-byte nonce, base64",
      "c": "AES-GCM ciphertext (nonce not included in c), base64"
    },
    "plaintext": "UTF-8 JSON object; compact serialization preferred.",
    "direction": {
      "client_to_server": "encrypt with HKDF-derived c2s key",
      "server_to_client": "encrypt with HKDF-derived s2c key"
    }
  },
  "data_frame_1_1": {
    "wire": "1.1",
    "transport": "WebSocket binary frame (opcode 0x2)",
    "layout": {
      "offset_0": "uint8 ver = 0x11",
      "offset_1": "uint8 ptype",
      "offset_2": "12 bytes nonce",
      "offset_14": "ciphertext || 16-byte GCM tag"
    },
    "no_json": true,
    "no_base64": true,
    "length": "Implied by WebSocket frame length; no separate length field.",
    "direction": {
      "client_to_server": "encrypt with c2s",
      "server_to_client": "encrypt with s2c"
    },
    "ptypes": {
      "0x01": "APP_JSON — UTF-8 JSON application object (chat/RPC)",
      "0x02": "APP_BIN — opaque binary",
      "0x03": "TEXT — UTF-8 text",
      "0x10": "FILE_BEGIN",
      "0x11": "FILE_CHUNK",
      "0x12": "FILE_END",
      "0x13": "FILE_ABORT",
      "0x14": "FILE_ACK",
      "0x15": "FILE_RESUME"
    }
  },
  "file_transfer_1_1": {
    "model": "Chunked: FILE_BEGIN → FILE_CHUNK* → FILE_END; each chunk is a separate binary frame encrypted with its own AES-GCM nonce/tag.",
    "resume": "Receiver tracks last contiguous chunk index; sender may send FILE_RESUME(from_chunk); receiver answers FILE_ACK(status=need_resume|ok, last_ok).",
    "integrity": "Per-chunk AEAD tag + optional whole-file SHA-256 in FILE_BEGIN/FILE_END.",
    "default_chunk_size": 262144,
    "plaintext": {
      "FILE_BEGIN": "transfer_id[16] | file_size u64be | chunk_size u32be | total_chunks u32be | flags u8 | name_len u16be | name | mime_len u16be | mime | [sha256 32 if flags&1]",
      "FILE_CHUNK": "transfer_id[16] | chunk_index u32be | data",
      "FILE_END": "transfer_id[16] | sha256[32]",
      "FILE_ABORT": "transfer_id[16] | reason_code u8",
      "FILE_ACK": "transfer_id[16] | status u8 | last_ok u32be",
      "FILE_RESUME": "transfer_id[16] | from_chunk u32be"
    },
    "ack_status": ["ok_continue", "need_resume", "complete", "error"],
    "payloads": "Any user file: binary, text, images, documents, audio, video, archives."
  },
  "reference_downloads": {
    "python3": "./reference.py",
    "browser_esm": "./reference-client.js"
  },
  "developer_experience": {
    "goal": "Same crypto as the wire format, but clearer steps and optional conventions so integrations look like a small SDK rather than raw frame assembly.",
    "client_python": [
      "DVeClientSession.from_server_hello_text(hello_json, prefer=\"1.1\") -> (session, client_ack_json)",
      "session.pack_outgoing(app_obj) / unpack_incoming(frame) — returns str (1.0) or bytes (1.1)",
      "session.pack_file_begin / pack_file_chunk / pack_file_end / pack_file_resume",
      "developer_server_session_from_ack(server_private, client_ack_text) -> DVeSession"
    ],
    "client_javascript": [
      "dveDeveloperHandshake(hello, prefer?) -> { session, clientAckJson, wire }",
      "session.encryptOutgoing / decryptIncoming — string (1.0) or Uint8Array (1.1)",
      "dvePackFileBegin / dvePackFileChunk / dvePackFileEnd helpers",
      "dveAppMessage(op, extra?, seq?) and DVeSeq for optional dve_op / dve_seq in APP_JSON plaintext"
    ],
    "app_message_convention": {
      "dve_op": "string, logical RPC or message name agreed between client and server",
      "dve_seq": "optional int, monotonic per session for ordering (use DVeSeq in Python / JS)",
      "note": "For wire 1.1 APP_JSON, plaintext JSON inside AES-GCM; fields are authenticated by AEAD."
    }
  },
  "compatibility": {
    "v1_clients": "Ignore unknown hello.offer; omit select → server uses wire 1.0 JSON frames.",
    "v1_1_clients": "If server omits offer, select 1.0. If offer includes 1.1, select 1.1 and use binary frames only after ack.",
    "mixed": "Server keeps both modes per connection based on select."
  },
  "exchange_api": {
    "url": "https://api.f-chat.ru/dve_exchange.php",
    "openapi": "https://api.f-chat.ru/dve_exchange.openapi.yaml",
    "auth": "Same session cookies as feed (secure_session + auth_token).",
    "routing_modes": {
      "e2e": "Device encrypts → server stores opaque payload → recipient decrypts. Server never sees plaintext.",
      "gateway": "Device encrypts with server RSA public key (hybrid RSA-OAEP + AES-256-GCM) → server decrypts and stores plaintext for recipient. Use only if you accept server-readable content."
    },
    "send_e2e": "POST JSON { action: \"send\", routing_mode: \"e2e\" (default), to_user_hash, payload, client_msg_id?: \"...\" }",
    "send_gateway": "POST JSON { action: \"send\", routing_mode: \"gateway\", to_user_hash, gateway_envelope: { ek, iv, ct [, kid] } }",
    "inbox": "GET ?action=inbox&after_id=0&limit=50&unread=1",
    "outbox": "GET ?action=outbox&after_id=0",
    "ack_read": "POST JSON { action: \"ack_read\", ids: [1,2,3] }",
    "privacy": "Do not log e2e payloads in debug output; gateway mode implies server can read plaintext after decrypt."
  },
  "machine_readable_spec_url": "https://api.f-chat.ru/DVeProto",
  "legacy_spec_url": "https://api.f-chat.ru/dveproto_spec.php"
}
