/* * antpeer.hpp — Peer library for Antheos bus communication. * * Pure C++17. Transport abstraction (MemBus/SockBus), Server * (service endpoint), Client (service consumer). * * Copyright (c) 2025-2026 Are Bjørby * SPDX-License-Identifier: MIT */ #pragma once #include #include #include #include #include #include #include namespace antpeer { /* ── Constants ─────────────────────────────────────────────────────── */ inline constexpr size_t MEMBUS_DEFAULT_SIZE = 256 * 1024; inline constexpr size_t BUS_MAX_NAME = 256; inline constexpr int MAX_SESSIONS = 64; inline constexpr int MAX_PEERS = 64; inline constexpr int MAX_OFFERS = 16; /* ── Bus (abstract, RAII) ──────────────────────────────────────────── */ class Bus { public: virtual ~Bus() = default; Bus(const Bus&) = delete; Bus& operator=(const Bus&) = delete; virtual ssize_t write(const void* data, size_t len) = 0; virtual ssize_t read(void* buffer, size_t len) = 0; virtual bool reopen() = 0; virtual bool is_open() const = 0; virtual std::string_view name() const = 0; protected: Bus() = default; }; /** * Open a membus-backed transport. * Handles stale SHM detection and create-if-needed internally. * @throws std::system_error on failure */ std::unique_ptr membus_open(std::string_view name, size_t size = MEMBUS_DEFAULT_SIZE); /** * Open a sockbus-backed transport. * Creates the broker if it doesn't exist (ignores EEXIST). * @throws std::system_error on failure */ std::unique_ptr sockbus_open(std::string_view name); /** * Open a blebus-backed transport (BLE L2CAP CoC). * Creates the broker if it doesn't exist (ignores EEXIST). * @throws std::system_error on failure */ std::unique_ptr blebus_open(std::string_view name); /* ── Named-object removal ──────────────────────────────────────────── */ /* * THE `*_open` CALLS ABOVE CREATE A NAMED KERNEL OBJECT AND THE `Bus` * DESTRUCTOR DOES NOT REMOVE IT. That is deliberate, not an omission: a * named POSIX shm object outliving the process that created it is the * point of naming one, and unlinking on handle close would destroy a * rendezvous that other peers are still seeking. The same holds for the * sockbus broker and the blebus endpoint. * * So removal has to be a separate, explicit call — and until 3.3.0 this * library wrapped the create half of all three transports and the remove * half of none, three times over. A consumer that opened a bus through * antpeer had to `#include ` and reach past the library * it was using in order to close the loop. * * `Client::close()` IS NOT THE PAIR FOR THESE, and its name invites the * mistake. It ends the session and drops this process's handle; the named * object is untouched and outlives it. Read `close` as "I am done with * this bus" and `*_destroy` as "this bus should no longer exist". * * Each forwards to its transport's own `destroy`. Removing a name that is * not there is not an error in any of the three, so these are safe to call * on an already-removed bus and safe to call twice. * * @throws std::system_error if the underlying transport reports one */ void membus_destroy(std::string_view name); void sockbus_destroy(std::string_view name); void blebus_destroy(std::string_view name); /* ── Session state ─────────────────────────────────────────────────── */ enum class SessionState { Disconnected = 0, Connected = 1, Suspended = 2 }; /* ── Peer identity (resolved via Verify) ───────────────────────────── */ struct PeerIdentity { std::string bid; std::string oid; std::string did; std::string iid; bool verified() const { return !oid.empty(); } }; /* ── Route (multi-hop relay addressing) ────────────────────────────── */ struct Route { std::string path; /* dot-separated BID path */ uint32_t my_index; /* our position in the path */ }; /* ── Server ────────────────────────────────────────────────────────── */ class Server { public: using RequestFn = std::function; using SessionFn = std::function; using FinishFn = std::function; using NotifyFn = std::function; using AcceptFn = std::function; using VerifyFn = std::function; using AuthFn = std::function; Server(); ~Server(); Server(const Server&) = delete; Server& operator=(const Server&) = delete; int init(Bus& bus, std::string_view oid, std::string_view did, std::string_view iid, std::string_view service_name); void on_request(RequestFn fn); void on_session(SessionFn fn); void on_finish(FinishFn fn); void on_notify(NotifyFn fn); void on_accept(AcceptFn fn); void on_verify(VerifyFn fn); void on_auth(AuthFn fn); void set_session_expiry(int seconds); /** * Require Ed25519 authentication for connecting peers. * Loads trusted public keys from config file. After Verify exchange, * server sends Z-verb challenge. Peer must sign with a trusted key. * @return 0 on success, -1 on error (file not found, parse error) */ int require_auth(const char* trusted_keys_path); /** * Register a route to a remote peer for relay-based Z-verb auth. * When the server needs to send a Z-challenge to this peer, it wraps * the challenge in a Relay frame with the given path and index. */ void add_route(std::string_view target_bid, std::string_view path, uint32_t my_index); int reply(std::string_view sid, std::string_view body); int reply_with_blob(std::string_view sid, std::string_view body, const uint8_t* blob, size_t blob_len); int notify(std::string_view sid, std::string_view event); int broadcast(std::string_view event); int finish(std::string_view sid); int offer_additional(std::string_view description); /** Send Verify request to a peer BID. */ int send_verify(std::string_view bid); /** * Enable threaded request dispatch. When n > 0, request_fn callbacks * run on a worker pool instead of inline in the poll loop. Workers * queue replies back; the poll loop drains them (all ctx_ access stays * single-threaded). Default n=0: inline dispatch (current behaviour). * Must be called before run(). */ void set_dispatch_threads(size_t n); void run(); void stop(); const char* bid() const; /** Trace ID extracted from last received message (empty if none). */ std::string_view last_trace_id() const; /** BLOB tail from the last received message ({nullptr,0} if none). * Valid during on_request callback (inline dispatch) or worker thread. */ std::pair last_blob_tail() const; /** Look up cached identity for a peer BID (nullptr if unknown). */ const PeerIdentity* peer_identity(std::string_view bid) const; /** Resolve SID to peer BID (empty if unknown). */ std::string_view peer_bid_for_session(std::string_view sid) const; /** Check if a peer BID has passed Z-verb authentication. */ bool is_authenticated(std::string_view bid) const; private: struct Impl; std::unique_ptr impl_; }; /* ── Client ────────────────────────────────────────────────────────── */ class Client { public: using NotifyFn = std::function; using ShutdownFn = std::function; Client(); ~Client(); Client(const Client&) = delete; Client& operator=(const Client&) = delete; int init(Bus& bus, std::string_view oid, std::string_view did, std::string_view iid); int discover(std::string_view service, int timeout_ms = 5000); int call(std::string_view command, int timeout_ms, char* reply, size_t reply_max); std::string call(std::string_view command, int timeout_ms = 30000); int fire(std::string_view command); bool connected() const; SessionState state() const; int ensure_connected(int timeout_ms = 5000); void reconnect(); void close(); void on_notify(NotifyFn fn); void on_shutdown(ShutdownFn fn); void set_required(bool required); /** * Set Ed25519 private key for auth responses. * When a server sends a Z-verb challenge, client auto-signs with this key. * @return 0 on success, -1 on error (file not found, parse error) */ int set_auth_key(const char* private_key_path); /** * Set relay route to server for multi-hop Z-verb auth. * When the client receives a relayed Z-challenge, it responds via relay * using this path. The index is the client's position in the path. */ void set_route(std::string_view path, uint32_t my_index); /** Set trace ID to propagate with next call()/fire(). */ void set_trace_id(std::string_view id); void clear_trace_id(); std::string_view trace_id() const; const char* peer_bid() const; private: struct Impl; std::unique_ptr impl_; }; /* ── FrameExtractor ────────────────────────────────────────────────── */ /** * Protocol-correct Antheos frame extractor for unframed byte streams. * * Extracts complete Antheos frames (head + optional BLOB tail) from a * raw byte stream. Uses antheos::Parser for head parsing and BLOB word * detection, then consumes tail bytes directly based on declared sizes. * * Handles arbitrarily large tails (no internal buffer limit). Emits * each complete frame via the on_frame callback. */ class FrameExtractor { public: using FrameCb = std::function; FrameExtractor(); ~FrameExtractor(); FrameExtractor(const FrameExtractor&) = delete; FrameExtractor& operator=(const FrameExtractor&) = delete; FrameExtractor(FrameExtractor&&) noexcept; FrameExtractor& operator=(FrameExtractor&&) noexcept; void on_frame(FrameCb cb); void feed(const uint8_t* data, size_t len); void reset(); size_t total_frames() const; size_t parse_errors() const; private: struct Impl; std::unique_ptr impl_; }; } // namespace antpeer