mail4agent_messenger/lib.rs
1//! `mail4agent-messenger` — sans-I/O Matrix Client-Server-subset sync + E2EE
2//! (Olm/Megolm) engine. Shells execute [`wire::OutgoingRequest`] and persist
3//! the sealed records this crate exports. No network and no disk inside the
4//! library.
5//!
6//! # Contract
7//!
8//! This crate performs **no I/O of any kind**: no network sockets, no
9//! filesystem access, no browser storage calls, and no `async`/`.await`
10//! anywhere in its source (plan §0). Every side effect crosses the
11//! boundary as plain data:
12//!
13//! - **Network**: the core hands out [`wire::OutgoingRequest`] values; a
14//! shell (native or web) performs the actual HTTP call and feeds the
15//! result back in as an [`wire::HttpResponseDescriptor`]. The core never
16//! opens a socket itself.
17//! - **Disk/persistence**: the core owns an in-memory working set and
18//! marks records dirty as it mutates state; [`persist::FlushGate`]
19//! batches dirty/deleted records into [`persist::FlushBatch`] values a
20//! shell writes to durable storage (the filesystem on native, IndexedDB
21//! on web) and acks back in. The core never calls a storage API itself.
22//!
23//! # Flush-before-send barrier
24//!
25//! [`persist::FlushEpoch`] is the barrier the plan's manager decision #3
26//! requires: a state mutation (an Olm/Megolm ratchet advance, most
27//! critically) must be durably flushed and acknowledged *before* the
28//! ciphertext that used it is allowed to leave the process — scoped
29//! per-request (M12's [`persist::RequiredSeq`] epoch) rather than as one
30//! global barrier, so an unrelated `/sync` or typing indicator is never
31//! held behind it. See `persist`'s module doc for the exact ordering
32//! guarantee this makes, with a worked example.
33//!
34//! # Synchronous by construction
35//!
36//! No function in this crate's public API returns a `Future` or is
37//! declared `async fn`. This is what lets the same source compile and
38//! unit-test identically on native and `wasm32-unknown-unknown`: the only
39//! platform-divergent code is which transport/executor a *shell* wires in,
40//! never this crate.
41//!
42//! # Pieces landed so far (M1, M2)
43//!
44//! **M1** — the wire-boundary primitives: Matrix identifier newtypes
45//! ([`ids`]), this crate's error type ([`error`]), the
46//! `OutgoingRequest`/`HttpResponseDescriptor` shapes ([`wire`]), and the
47//! flush-before-send barrier's data structures ([`persist`]).
48//!
49//! **M2** — the working-set store ([`store`]): [`store::Store`], the
50//! in-memory `CryptoStore`/`StateStore` implementation later pieces
51//! (crypto, room, sync engine) code against, sealed for export through
52//! [`persist::FlushGate`] by a [`store::RecordCodec`] (this piece ships only
53//! `store::InsecurePlainCodecForTests`, gated behind the `test-support`
54//! Cargo feature; the real AES-256-GCM codec is a later piece, M14).
55//!
56//! **M3** — the `/sync` and event wire types ([`wire::events`],
57//! [`wire::sync`]): [`wire::sync::SyncResponse`] and
58//! [`wire::sync::parse_sync_response`], the Matrix event envelope
59//! ([`wire::events::RawEvent`], [`wire::events::StrippedStateEvent`],
60//! [`wire::events::ToDeviceEvent`]), relation aggregation
61//! ([`wire::events::RelatesTo`]), and typed content for the state/timeline/
62//! to-device/account-data/ephemeral event types this crate reads.
63//!
64//! **M5** — Matrix canonical-JSON signing ([`canonical_json`]:
65//! [`canonical_json::sign_json`], [`canonical_json::verify_json_signature`])
66//! and the device's own Olm account ([`crypto::account::OlmAccountState`]):
67//! generate-once-and-persist identity, one-time/fallback key maintenance off
68//! `/sync`'s own counters, and the signed `device_keys`/`/keys/upload`
69//! request bodies built from it.
70//!
71//! **M4** — room state, timeline assembly, and relation aggregation
72//! ([`room`]): [`room::RoomState`] folded from `m.room.*` state events plus
73//! [`room::RoomKind`] derivation, [`room::Timeline`] assembled from `/sync`
74//! windows and `/messages` back-pages with local-echo reconciliation and
75//! live v11-allow-list redaction, and [`room::RelationsBundle`] aggregation
76//! (reactions, edits, replies, threads).
77//!
78//! **M6** — device-list tracking ([`crypto::device_tracker`]) and per-device
79//! Olm 1:1 sessions ([`crypto::olm_sessions`]): `/keys/query`/`/keys/claim`
80//! request building and response validation, to-device encryption/
81//! decryption with full payload binding, and the security checks that keep
82//! a key change or an unverified device from being silently trusted.
83//!
84//! **M7** — Megolm group sessions ([`crypto::group_sessions`]): lazy
85//! outbound-session creation and rotation, room-key sharing batched over
86//! Olm to-device, and inbound decrypt with bound-sender authentication and
87//! message-index replay protection.
88//!
89//! **M12** — the outgoing request queue ([`outgoing_queue`]):
90//! [`outgoing_queue::OutgoingQueue`] tracks pending requests per
91//! [`outgoing_queue::Lane`] (`Sync`/`ToDevice`/`Room`/`Other`), replaying
92//! them idempotently with capped exponential backoff
93//! ([`outgoing_queue::ResponseOutcome`]) and surviving a crash via
94//! [`store::StateStore::pending_requests`]. [`persist::FlushGate`] gained a
95//! per-request flush epoch ([`persist::FlushEpoch`], [`persist::RequiredSeq`])
96//! replacing its earlier global barrier — see `persist`'s module doc.
97//!
98//! **M13a** — the single-writer kernel and the RECEIVE path
99//! ([`core::MessengerCore`]): `open`/restart restore (including the
100//! [`core::Counters`] sequence that backs every future
101//! [`ids::RequestId`]/[`ids::TxnId`]), the single-flight `/sync` loop, full
102//! ingestion order (to-device decrypt and `m.room_key` routing,
103//! device-list/OTK/`keys/query` bookkeeping, room state/timeline/Megolm
104//! decrypt, ephemeral/account-data folding), and the receive-side
105//! [`core::MessengerCommand`] variants (`MarkRead`, `SetTyping`,
106//! `RetryDecryption`, `LoadOlder`).
107//!
108//! **M13b** — the SEND path, in the same module ([`core::MessengerCore`]):
109//! a per-room ordered send pipeline for [`core::MessengerCommand::SendMessage`]/
110//! `React`/`Redact`/`RetrySend` (key-query → key-claim → key-share → the
111//! room event itself, for an encrypted room; straight to the wire
112//! otherwise — key sharing is always awaited before the room event that
113//! needs it), plus every room-membership/tag/typing command
114//! (`CreateRoom`, `JoinRoom`, `LeaveRoom`, `Invite`, `Kick`, `SetTag`,
115//! `RemoveTag`, `SetTyping`'s own debounce). Plan §3's separate
116//! `sync_engine.rs` never materialized as its own module — everything it
117//! described lives directly in [`core`] instead, alongside the kernel it
118//! would otherwise have had to be threaded through anyway.
119//!
120//! **M14** — at-rest sealing ([`store::sealed::SealedRecordCodec`]): the
121//! production [`store::RecordCodec`], AES-256-GCM under a caller-supplied
122//! 32-byte [`core::CoreSecrets::store_seal_key`], binding each record's own
123//! storage key as AAD. [`core::MessengerCore::open_sealed`] is the
124//! constructor a shell calls; [`core::MessengerCore::open`] stays generic so
125//! this crate's own tests keep using [`store::InsecurePlainCodecForTests`].
126//! The session private key is the Olm account
127//! [`crypto::account::OlmAccountState`] already generates. This crate does
128//! not derive keys, read a passphrase, or own an identity scheme.
129
130#![forbid(unsafe_code)]
131#![deny(missing_docs)]
132
133pub mod canonical_json;
134pub mod core;
135pub mod crypto;
136pub mod error;
137pub mod ids;
138pub mod outgoing_queue;
139pub mod persist;
140pub mod persist_log;
141pub mod room;
142pub mod store;
143pub mod wire;
144
145pub use canonical_json::{sign_json, verify_json_signature, CanonicalJsonError};
146pub use core::{
147 CoreConfig, CoreSecrets, Counters, CreateRoomKind, MessageKind, MessengerCommand, MessengerCore, MessengerEvent,
148 OutgoingMessage, PublicRoomsResultEntry, UserDirectoryResultEntry,
149};
150pub use crypto::account::OlmAccountState;
151pub use crypto::device_tracker::{DeviceKeyChanged, DeviceTracker, DroppedDevice, KeysQueryOutcome, StoredDevice};
152pub use crypto::group_sessions::{
153 GroupDecryptError, GroupSessionManager, OutboundSessionUpdate, RoomEventPlaintext, UtdReason,
154 MAX_DEVICES_PER_SEND_TO_DEVICE_REQUEST,
155};
156pub use crypto::olm_sessions::{
157 DecryptedToDevice, KeysClaimOutcome, KeysClaimRefusal, OlmDecryptError, OlmSessionManager,
158};
159pub use error::MessengerError;
160pub use ids::{DeviceId, EventId, RequestId, RoomId, TxnId, UserId};
161pub use outgoing_queue::{Jitter, Lane, OutgoingQueue, PendingRequest, ResponseOutcome};
162pub use persist::{FlushBatch, FlushEpoch, FlushGate, RecordKey, RequiredSeq, SealedRecord};
163pub use room::{
164 aggregate_relation, highlight, last_preview, recompute_bundle, EditRecord, Forwarded, GapMarker, ItemContent,
165 MemberRole, MemberState, RelatingEvent, RelationsBundle, RoomKind, RoomState, SendState, StateChangeSummary,
166 Timeline, TimelineItem,
167};
168pub use store::sealed::SealedRecordCodec;
169pub use store::{CryptoStore, RecordCodec, StateStore, Store, StoreError};
170pub use wire::{HttpMethod, HttpResponseDescriptor, OutgoingRequest, OutgoingRequestKind};