2026-02-11 14:10:21 -08:00
|
|
|
use blake2::{
|
|
|
|
|
Blake2b, Blake2bMac, Digest,
|
|
|
|
|
digest::{FixedOutput, consts::U18},
|
|
|
|
|
};
|
2026-01-22 06:39:09 +07:00
|
|
|
use chat_proto::logoschat::{
|
|
|
|
|
convos::private_v1::{PrivateV1Frame, private_v1_frame::FrameType},
|
|
|
|
|
encryption::{Doubleratchet, EncryptedPayload, encrypted_payload::Encryption},
|
|
|
|
|
};
|
2026-02-18 09:29:33 -08:00
|
|
|
use crypto::{PrivateKey, PublicKey, SymmetricKey32};
|
2026-04-10 08:33:58 +08:00
|
|
|
use double_ratchets::{Header, InstallationKeyPair, RatchetState, restore_ratchet_state};
|
feat: introduce client event system (#106)
* chore(flake): accept extra system attr; add perl for openssl-sys build
forAllSystems calls the lambda with {system, pkgs}; strict
destructuring requires `..` to ignore the system attribute.
`pkgs.perl` is needed because openssl-sys is pulled vendored via
libsqlite3-sys / rusqlite / chat-sqlite, and its `perl Configure`
step needs FindBin.pm, which Fedora's system perl doesn't ship.
* feat: introduce client event system
- Core processing yields a `PayloadOutcome` enum — `Empty`, `Convo`, or
`Inbox`. `ConvoOutcome` carries a conversation id and an optional
decrypted `Content`; `InboxOutcome` adds a `NewConversation`
(id + `ConversationClass`) for a peer-initiated conversation.
- Client translates `PayloadOutcome` into app-facing `Vec<Event>`
(`ConversationStarted`, `MessageReceived`) at the boundary, so the
application loop sees discrete events rather than core types.
- MLS group welcomes produce a `ConversationStarted` event with no
initial content, fixing the silent-group-join case where the inbox
layer dropped the observation.
- C FFI exposes an `EventList` opaque type with indexed accessors and
an `Invalid` sentinel for out-of-bounds / non-applicable reads.
- Symmetric `Inbox` / `InboxV2` handlers: both return
`Result<InboxOutcome, _>` and own the persistence + ephemeral-key
cleanup for the conversations they create.
- Updated and simplified `docs/adr/0001-client-event-system.md`.
* chore(flake): bump nixpkgs to nixos-unstable-small
Temporary. The two crates.io UA fixes (NixOS/nixpkgs#512735 for
fetchCargoVendor's python-requests UA, NixOS/nixpkgs#524985 for
importCargoLock's curl UA) haven't propagated to nixos-unstable yet.
Switch to nixos-unstable-small and force logos-delivery to follow so
the smoketest gets the same fix. Revert once nixos-unstable catches up.
Refs:
- https://github.com/rust-lang/crates.io/issues/13482
- https://github.com/rust-lang/crates.io/issues/13783
- https://crates.io/data-access
2026-05-28 23:51:15 +02:00
|
|
|
use prost::{Message as _, bytes::Bytes};
|
refactor(core): replace Rc-based Context with a synchronous, Send-able Core (#123)
Make the conversations core Send so the threaded client can own it behind an
Arc<Mutex<Core>>: a background worker polls the transport and handles inbound
payloads while the application thread issues outbound calls (send, create
conversation). Sharing the core across those two threads means moving it into
the spawned worker, which is only legal if it is Send. Access stays serialized
by the client's Mutex (one thread at a time), so the core needs Send but not
Sync and carries no lock of its own. See
docs/adr/0001-client-event-system.md for the background-poller design.
The Rc<RefCell> service-sharing is what made the core !Send. Context is de-Rc'd
and renamed to Core, owning its services outright and driving the inbox and
conversation primitives with plain &mut self.
- Services (identity, delivery, store, registry, MLS context, causal history)
are bundled into a ServiceContext<S> behind an ExternalServices trait, with
S = (DS, RS, CS). Constructors live on the (DS, RS, CS) form because S cannot
be inferred backwards through S::DS.
- Inbox, InboxV2, PrivateV1Convo, and GroupV1Convo become non-generic and
receive the ServiceContext bundle as a &mut/& parameter; no Rc or
RefCell-as-shared-state remains, so Core is Send whenever its injected
services are.
- Dispatch branches on ConversationKind in one place: Core rebuilds the target
as a Convo<S>/GroupConvo<S> trait object bound to the service bundle, so
conversations never escape the orchestrator.
- CausalHistoryStore drops its Rc, keeping a plain RefCell.
2026-06-08 21:55:33 +02:00
|
|
|
use std::fmt::Debug;
|
2026-04-10 08:33:58 +08:00
|
|
|
use storage::{ConversationKind, ConversationMeta, ConversationStore};
|
2026-01-22 06:39:09 +07:00
|
|
|
|
|
|
|
|
use crate::{
|
refactor(core): replace Rc-based Context with a synchronous, Send-able Core (#123)
Make the conversations core Send so the threaded client can own it behind an
Arc<Mutex<Core>>: a background worker polls the transport and handles inbound
payloads while the application thread issues outbound calls (send, create
conversation). Sharing the core across those two threads means moving it into
the spawned worker, which is only legal if it is Send. Access stays serialized
by the client's Mutex (one thread at a time), so the core needs Send but not
Sync and carries no lock of its own. See
docs/adr/0001-client-event-system.md for the background-poller design.
The Rc<RefCell> service-sharing is what made the core !Send. Context is de-Rc'd
and renamed to Core, owning its services outright and driving the inbox and
conversation primitives with plain &mut self.
- Services (identity, delivery, store, registry, MLS context, causal history)
are bundled into a ServiceContext<S> behind an ExternalServices trait, with
S = (DS, RS, CS). Constructors live on the (DS, RS, CS) form because S cannot
be inferred backwards through S::DS.
- Inbox, InboxV2, PrivateV1Convo, and GroupV1Convo become non-generic and
receive the ServiceContext bundle as a &mut/& parameter; no Rc or
RefCell-as-shared-state remains, so Core is Send whenever its injected
services are.
- Dispatch branches on ConversationKind in one place: Core rebuilds the target
as a Convo<S>/GroupConvo<S> trait object bound to the service bundle, so
conversations never escape the orchestrator.
- CausalHistoryStore drops its Rc, keeping a plain RefCell.
2026-06-08 21:55:33 +02:00
|
|
|
DeliveryService,
|
2026-06-19 08:43:55 -07:00
|
|
|
conversation::{ChatError, ConversationId, ConversationIdRef, Convo, Identified},
|
2026-02-04 06:17:45 +07:00
|
|
|
errors::EncryptionError,
|
refactor(core): replace Rc-based Context with a synchronous, Send-able Core (#123)
Make the conversations core Send so the threaded client can own it behind an
Arc<Mutex<Core>>: a background worker polls the transport and handles inbound
payloads while the application thread issues outbound calls (send, create
conversation). Sharing the core across those two threads means moving it into
the spawned worker, which is only legal if it is Send. Access stays serialized
by the client's Mutex (one thread at a time), so the core needs Send but not
Sync and carries no lock of its own. See
docs/adr/0001-client-event-system.md for the background-poller design.
The Rc<RefCell> service-sharing is what made the core !Send. Context is de-Rc'd
and renamed to Core, owning its services outright and driving the inbox and
conversation primitives with plain &mut self.
- Services (identity, delivery, store, registry, MLS context, causal history)
are bundled into a ServiceContext<S> behind an ExternalServices trait, with
S = (DS, RS, CS). Constructors live on the (DS, RS, CS) form because S cannot
be inferred backwards through S::DS.
- Inbox, InboxV2, PrivateV1Convo, and GroupV1Convo become non-generic and
receive the ServiceContext bundle as a &mut/& parameter; no Rc or
RefCell-as-shared-state remains, so Core is Send whenever its injected
services are.
- Dispatch branches on ConversationKind in one place: Core rebuilds the target
as a Convo<S>/GroupConvo<S> trait object bound to the service bundle, so
conversations never escape the orchestrator.
- CausalHistoryStore drops its Rc, keeping a plain RefCell.
2026-06-08 21:55:33 +02:00
|
|
|
inbox::PRIVATE_V1_INBOX_ADDRESS,
|
feat: introduce client event system (#106)
* chore(flake): accept extra system attr; add perl for openssl-sys build
forAllSystems calls the lambda with {system, pkgs}; strict
destructuring requires `..` to ignore the system attribute.
`pkgs.perl` is needed because openssl-sys is pulled vendored via
libsqlite3-sys / rusqlite / chat-sqlite, and its `perl Configure`
step needs FindBin.pm, which Fedora's system perl doesn't ship.
* feat: introduce client event system
- Core processing yields a `PayloadOutcome` enum — `Empty`, `Convo`, or
`Inbox`. `ConvoOutcome` carries a conversation id and an optional
decrypted `Content`; `InboxOutcome` adds a `NewConversation`
(id + `ConversationClass`) for a peer-initiated conversation.
- Client translates `PayloadOutcome` into app-facing `Vec<Event>`
(`ConversationStarted`, `MessageReceived`) at the boundary, so the
application loop sees discrete events rather than core types.
- MLS group welcomes produce a `ConversationStarted` event with no
initial content, fixing the silent-group-join case where the inbox
layer dropped the observation.
- C FFI exposes an `EventList` opaque type with indexed accessors and
an `Invalid` sentinel for out-of-bounds / non-applicable reads.
- Symmetric `Inbox` / `InboxV2` handlers: both return
`Result<InboxOutcome, _>` and own the persistence + ephemeral-key
cleanup for the conversations they create.
- Updated and simplified `docs/adr/0001-client-event-system.md`.
* chore(flake): bump nixpkgs to nixos-unstable-small
Temporary. The two crates.io UA fixes (NixOS/nixpkgs#512735 for
fetchCargoVendor's python-requests UA, NixOS/nixpkgs#524985 for
importCargoLock's curl UA) haven't propagated to nixos-unstable yet.
Switch to nixos-unstable-small and force logos-delivery to follow so
the smoketest gets the same fix. Revert once nixos-unstable catches up.
Refs:
- https://github.com/rust-lang/crates.io/issues/13482
- https://github.com/rust-lang/crates.io/issues/13783
- https://crates.io/data-access
2026-05-28 23:51:15 +02:00
|
|
|
outcomes::{Content, ConvoOutcome},
|
2026-02-04 06:17:45 +07:00
|
|
|
proto,
|
refactor(core): replace Rc-based Context with a synchronous, Send-able Core (#123)
Make the conversations core Send so the threaded client can own it behind an
Arc<Mutex<Core>>: a background worker polls the transport and handles inbound
payloads while the application thread issues outbound calls (send, create
conversation). Sharing the core across those two threads means moving it into
the spawned worker, which is only legal if it is Send. Access stays serialized
by the client's Mutex (one thread at a time), so the core needs Send but not
Sync and carries no lock of its own. See
docs/adr/0001-client-event-system.md for the background-poller design.
The Rc<RefCell> service-sharing is what made the core !Send. Context is de-Rc'd
and renamed to Core, owning its services outright and driving the inbox and
conversation primitives with plain &mut self.
- Services (identity, delivery, store, registry, MLS context, causal history)
are bundled into a ServiceContext<S> behind an ExternalServices trait, with
S = (DS, RS, CS). Constructors live on the (DS, RS, CS) form because S cannot
be inferred backwards through S::DS.
- Inbox, InboxV2, PrivateV1Convo, and GroupV1Convo become non-generic and
receive the ServiceContext bundle as a &mut/& parameter; no Rc or
RefCell-as-shared-state remains, so Core is Send whenever its injected
services are.
- Dispatch branches on ConversationKind in one place: Core rebuilds the target
as a Convo<S>/GroupConvo<S> trait object bound to the service bundle, so
conversations never escape the orchestrator.
- CausalHistoryStore drops its Rc, keeping a plain RefCell.
2026-06-08 21:55:33 +02:00
|
|
|
service_context::{ExternalServices, ServiceContext},
|
feat: introduce client event system (#106)
* chore(flake): accept extra system attr; add perl for openssl-sys build
forAllSystems calls the lambda with {system, pkgs}; strict
destructuring requires `..` to ignore the system attribute.
`pkgs.perl` is needed because openssl-sys is pulled vendored via
libsqlite3-sys / rusqlite / chat-sqlite, and its `perl Configure`
step needs FindBin.pm, which Fedora's system perl doesn't ship.
* feat: introduce client event system
- Core processing yields a `PayloadOutcome` enum — `Empty`, `Convo`, or
`Inbox`. `ConvoOutcome` carries a conversation id and an optional
decrypted `Content`; `InboxOutcome` adds a `NewConversation`
(id + `ConversationClass`) for a peer-initiated conversation.
- Client translates `PayloadOutcome` into app-facing `Vec<Event>`
(`ConversationStarted`, `MessageReceived`) at the boundary, so the
application loop sees discrete events rather than core types.
- MLS group welcomes produce a `ConversationStarted` event with no
initial content, fixing the silent-group-join case where the inbox
layer dropped the observation.
- C FFI exposes an `EventList` opaque type with indexed accessors and
an `Invalid` sentinel for out-of-bounds / non-applicable reads.
- Symmetric `Inbox` / `InboxV2` handlers: both return
`Result<InboxOutcome, _>` and own the persistence + ephemeral-key
cleanup for the conversations they create.
- Updated and simplified `docs/adr/0001-client-event-system.md`.
* chore(flake): bump nixpkgs to nixos-unstable-small
Temporary. The two crates.io UA fixes (NixOS/nixpkgs#512735 for
fetchCargoVendor's python-requests UA, NixOS/nixpkgs#524985 for
importCargoLock's curl UA) haven't propagated to nixos-unstable yet.
Switch to nixos-unstable-small and force logos-delivery to follow so
the smoketest gets the same fix. Revert once nixos-unstable catches up.
Refs:
- https://github.com/rust-lang/crates.io/issues/13482
- https://github.com/rust-lang/crates.io/issues/13783
- https://crates.io/data-access
2026-05-28 23:51:15 +02:00
|
|
|
types::AddressedEncryptedPayload,
|
2026-01-22 06:39:09 +07:00
|
|
|
utils::timestamp_millis,
|
|
|
|
|
};
|
2026-04-03 08:25:26 +08:00
|
|
|
use double_ratchets::{to_ratchet_record, to_skipped_key_records};
|
|
|
|
|
use storage::RatchetStore;
|
2025-12-22 09:40:46 -08:00
|
|
|
|
2026-02-11 14:10:21 -08:00
|
|
|
// Represents the potential participant roles in this Conversation
|
|
|
|
|
enum Role {
|
|
|
|
|
Initiator,
|
|
|
|
|
Responder,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Role {
|
|
|
|
|
const fn as_str(&self) -> &'static str {
|
|
|
|
|
match self {
|
|
|
|
|
Self::Initiator => "I",
|
|
|
|
|
Self::Responder => "R",
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
struct BaseConvoId([u8; 18]);
|
|
|
|
|
|
|
|
|
|
impl BaseConvoId {
|
2026-02-18 09:29:33 -08:00
|
|
|
fn new(key: &SymmetricKey32) -> Self {
|
|
|
|
|
let base = Blake2bMac::<U18>::new_with_salt_and_personal(key.as_bytes(), b"", b"L-PV1-CID")
|
2026-02-11 14:10:21 -08:00
|
|
|
.expect("fixed inputs should never fail");
|
|
|
|
|
Self(base.finalize_fixed().into())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn id_for_participant(&self, role: Role) -> String {
|
|
|
|
|
let hash = Blake2b::<U18>::new()
|
|
|
|
|
.chain_update(self.0)
|
|
|
|
|
.chain_update(role.as_str())
|
|
|
|
|
.finalize();
|
|
|
|
|
hex::encode(hash)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
refactor(core): replace Rc-based Context with a synchronous, Send-able Core (#123)
Make the conversations core Send so the threaded client can own it behind an
Arc<Mutex<Core>>: a background worker polls the transport and handles inbound
payloads while the application thread issues outbound calls (send, create
conversation). Sharing the core across those two threads means moving it into
the spawned worker, which is only legal if it is Send. Access stays serialized
by the client's Mutex (one thread at a time), so the core needs Send but not
Sync and carries no lock of its own. See
docs/adr/0001-client-event-system.md for the background-poller design.
The Rc<RefCell> service-sharing is what made the core !Send. Context is de-Rc'd
and renamed to Core, owning its services outright and driving the inbox and
conversation primitives with plain &mut self.
- Services (identity, delivery, store, registry, MLS context, causal history)
are bundled into a ServiceContext<S> behind an ExternalServices trait, with
S = (DS, RS, CS). Constructors live on the (DS, RS, CS) form because S cannot
be inferred backwards through S::DS.
- Inbox, InboxV2, PrivateV1Convo, and GroupV1Convo become non-generic and
receive the ServiceContext bundle as a &mut/& parameter; no Rc or
RefCell-as-shared-state remains, so Core is Send whenever its injected
services are.
- Dispatch branches on ConversationKind in one place: Core rebuilds the target
as a Convo<S>/GroupConvo<S> trait object bound to the service bundle, so
conversations never escape the orchestrator.
- CausalHistoryStore drops its Rc, keeping a plain RefCell.
2026-06-08 21:55:33 +02:00
|
|
|
pub struct PrivateV1Convo {
|
2026-02-11 14:10:21 -08:00
|
|
|
local_convo_id: String,
|
|
|
|
|
remote_convo_id: String,
|
2026-02-04 06:17:45 +07:00
|
|
|
dr_state: RatchetState,
|
|
|
|
|
}
|
2025-12-22 09:40:46 -08:00
|
|
|
|
refactor(core): replace Rc-based Context with a synchronous, Send-able Core (#123)
Make the conversations core Send so the threaded client can own it behind an
Arc<Mutex<Core>>: a background worker polls the transport and handles inbound
payloads while the application thread issues outbound calls (send, create
conversation). Sharing the core across those two threads means moving it into
the spawned worker, which is only legal if it is Send. Access stays serialized
by the client's Mutex (one thread at a time), so the core needs Send but not
Sync and carries no lock of its own. See
docs/adr/0001-client-event-system.md for the background-poller design.
The Rc<RefCell> service-sharing is what made the core !Send. Context is de-Rc'd
and renamed to Core, owning its services outright and driving the inbox and
conversation primitives with plain &mut self.
- Services (identity, delivery, store, registry, MLS context, causal history)
are bundled into a ServiceContext<S> behind an ExternalServices trait, with
S = (DS, RS, CS). Constructors live on the (DS, RS, CS) form because S cannot
be inferred backwards through S::DS.
- Inbox, InboxV2, PrivateV1Convo, and GroupV1Convo become non-generic and
receive the ServiceContext bundle as a &mut/& parameter; no Rc or
RefCell-as-shared-state remains, so Core is Send whenever its injected
services are.
- Dispatch branches on ConversationKind in one place: Core rebuilds the target
as a Convo<S>/GroupConvo<S> trait object bound to the service bundle, so
conversations never escape the orchestrator.
- CausalHistoryStore drops its Rc, keeping a plain RefCell.
2026-06-08 21:55:33 +02:00
|
|
|
impl PrivateV1Convo {
|
2026-04-03 08:25:26 +08:00
|
|
|
/// Reconstructs a PrivateV1Convo from persisted metadata and ratchet state.
|
refactor(core): replace Rc-based Context with a synchronous, Send-able Core (#123)
Make the conversations core Send so the threaded client can own it behind an
Arc<Mutex<Core>>: a background worker polls the transport and handles inbound
payloads while the application thread issues outbound calls (send, create
conversation). Sharing the core across those two threads means moving it into
the spawned worker, which is only legal if it is Send. Access stays serialized
by the client's Mutex (one thread at a time), so the core needs Send but not
Sync and carries no lock of its own. See
docs/adr/0001-client-event-system.md for the background-poller design.
The Rc<RefCell> service-sharing is what made the core !Send. Context is de-Rc'd
and renamed to Core, owning its services outright and driving the inbox and
conversation primitives with plain &mut self.
- Services (identity, delivery, store, registry, MLS context, causal history)
are bundled into a ServiceContext<S> behind an ExternalServices trait, with
S = (DS, RS, CS). Constructors live on the (DS, RS, CS) form because S cannot
be inferred backwards through S::DS.
- Inbox, InboxV2, PrivateV1Convo, and GroupV1Convo become non-generic and
receive the ServiceContext bundle as a &mut/& parameter; no Rc or
RefCell-as-shared-state remains, so Core is Send whenever its injected
services are.
- Dispatch branches on ConversationKind in one place: Core rebuilds the target
as a Convo<S>/GroupConvo<S> trait object bound to the service bundle, so
conversations never escape the orchestrator.
- CausalHistoryStore drops its Rc, keeping a plain RefCell.
2026-06-08 21:55:33 +02:00
|
|
|
pub fn new<S: ConversationStore + RatchetStore>(
|
|
|
|
|
store: &S,
|
2026-04-10 08:33:58 +08:00
|
|
|
local_convo_id: String,
|
|
|
|
|
remote_convo_id: String,
|
|
|
|
|
) -> Result<Self, ChatError> {
|
refactor(core): replace Rc-based Context with a synchronous, Send-able Core (#123)
Make the conversations core Send so the threaded client can own it behind an
Arc<Mutex<Core>>: a background worker polls the transport and handles inbound
payloads while the application thread issues outbound calls (send, create
conversation). Sharing the core across those two threads means moving it into
the spawned worker, which is only legal if it is Send. Access stays serialized
by the client's Mutex (one thread at a time), so the core needs Send but not
Sync and carries no lock of its own. See
docs/adr/0001-client-event-system.md for the background-poller design.
The Rc<RefCell> service-sharing is what made the core !Send. Context is de-Rc'd
and renamed to Core, owning its services outright and driving the inbox and
conversation primitives with plain &mut self.
- Services (identity, delivery, store, registry, MLS context, causal history)
are bundled into a ServiceContext<S> behind an ExternalServices trait, with
S = (DS, RS, CS). Constructors live on the (DS, RS, CS) form because S cannot
be inferred backwards through S::DS.
- Inbox, InboxV2, PrivateV1Convo, and GroupV1Convo become non-generic and
receive the ServiceContext bundle as a &mut/& parameter; no Rc or
RefCell-as-shared-state remains, so Core is Send whenever its injected
services are.
- Dispatch branches on ConversationKind in one place: Core rebuilds the target
as a Convo<S>/GroupConvo<S> trait object bound to the service bundle, so
conversations never escape the orchestrator.
- CausalHistoryStore drops its Rc, keeping a plain RefCell.
2026-06-08 21:55:33 +02:00
|
|
|
let dr_record = store.load_ratchet_state(&local_convo_id)?;
|
|
|
|
|
let skipped_keys = store.load_skipped_keys(&local_convo_id)?;
|
2026-04-10 08:33:58 +08:00
|
|
|
let dr_state: RatchetState = restore_ratchet_state(dr_record, skipped_keys);
|
|
|
|
|
|
|
|
|
|
Ok(Self {
|
2026-04-03 08:25:26 +08:00
|
|
|
local_convo_id,
|
|
|
|
|
remote_convo_id,
|
|
|
|
|
dr_state,
|
2026-04-10 08:33:58 +08:00
|
|
|
})
|
2026-04-03 08:25:26 +08:00
|
|
|
}
|
|
|
|
|
|
refactor(core): replace Rc-based Context with a synchronous, Send-able Core (#123)
Make the conversations core Send so the threaded client can own it behind an
Arc<Mutex<Core>>: a background worker polls the transport and handles inbound
payloads while the application thread issues outbound calls (send, create
conversation). Sharing the core across those two threads means moving it into
the spawned worker, which is only legal if it is Send. Access stays serialized
by the client's Mutex (one thread at a time), so the core needs Send but not
Sync and carries no lock of its own. See
docs/adr/0001-client-event-system.md for the background-poller design.
The Rc<RefCell> service-sharing is what made the core !Send. Context is de-Rc'd
and renamed to Core, owning its services outright and driving the inbox and
conversation primitives with plain &mut self.
- Services (identity, delivery, store, registry, MLS context, causal history)
are bundled into a ServiceContext<S> behind an ExternalServices trait, with
S = (DS, RS, CS). Constructors live on the (DS, RS, CS) form because S cannot
be inferred backwards through S::DS.
- Inbox, InboxV2, PrivateV1Convo, and GroupV1Convo become non-generic and
receive the ServiceContext bundle as a &mut/& parameter; no Rc or
RefCell-as-shared-state remains, so Core is Send whenever its injected
services are.
- Dispatch branches on ConversationKind in one place: Core rebuilds the target
as a Convo<S>/GroupConvo<S> trait object bound to the service bundle, so
conversations never escape the orchestrator.
- CausalHistoryStore drops its Rc, keeping a plain RefCell.
2026-06-08 21:55:33 +02:00
|
|
|
pub fn new_initiator(seed_key: SymmetricKey32, remote: PublicKey) -> Self {
|
2026-02-11 14:10:21 -08:00
|
|
|
let base_convo_id = BaseConvoId::new(&seed_key);
|
|
|
|
|
let local_convo_id = base_convo_id.id_for_participant(Role::Initiator);
|
|
|
|
|
let remote_convo_id = base_convo_id.id_for_participant(Role::Responder);
|
|
|
|
|
|
2026-02-18 09:29:33 -08:00
|
|
|
// TODO: Danger - Fix double-ratchets types to Accept SymmetricKey32
|
2026-02-04 06:17:45 +07:00
|
|
|
// perhaps update the DH to work with cryptocrate.
|
|
|
|
|
// init_sender doesn't take ownership of the key so a reference can be used.
|
2026-02-18 09:29:33 -08:00
|
|
|
let shared_secret: [u8; 32] = seed_key.DANGER_to_bytes();
|
|
|
|
|
let dr_state = RatchetState::init_sender(shared_secret, *remote);
|
2026-02-11 14:10:21 -08:00
|
|
|
|
2026-02-04 06:17:45 +07:00
|
|
|
Self {
|
2026-02-11 14:10:21 -08:00
|
|
|
local_convo_id,
|
|
|
|
|
remote_convo_id,
|
|
|
|
|
dr_state,
|
2026-02-04 06:17:45 +07:00
|
|
|
}
|
2025-12-22 09:40:46 -08:00
|
|
|
}
|
2026-01-22 06:39:09 +07:00
|
|
|
|
refactor(core): replace Rc-based Context with a synchronous, Send-able Core (#123)
Make the conversations core Send so the threaded client can own it behind an
Arc<Mutex<Core>>: a background worker polls the transport and handles inbound
payloads while the application thread issues outbound calls (send, create
conversation). Sharing the core across those two threads means moving it into
the spawned worker, which is only legal if it is Send. Access stays serialized
by the client's Mutex (one thread at a time), so the core needs Send but not
Sync and carries no lock of its own. See
docs/adr/0001-client-event-system.md for the background-poller design.
The Rc<RefCell> service-sharing is what made the core !Send. Context is de-Rc'd
and renamed to Core, owning its services outright and driving the inbox and
conversation primitives with plain &mut self.
- Services (identity, delivery, store, registry, MLS context, causal history)
are bundled into a ServiceContext<S> behind an ExternalServices trait, with
S = (DS, RS, CS). Constructors live on the (DS, RS, CS) form because S cannot
be inferred backwards through S::DS.
- Inbox, InboxV2, PrivateV1Convo, and GroupV1Convo become non-generic and
receive the ServiceContext bundle as a &mut/& parameter; no Rc or
RefCell-as-shared-state remains, so Core is Send whenever its injected
services are.
- Dispatch branches on ConversationKind in one place: Core rebuilds the target
as a Convo<S>/GroupConvo<S> trait object bound to the service bundle, so
conversations never escape the orchestrator.
- CausalHistoryStore drops its Rc, keeping a plain RefCell.
2026-06-08 21:55:33 +02:00
|
|
|
pub fn new_responder(seed_key: SymmetricKey32, dh_self: &PrivateKey) -> Self {
|
2026-02-11 14:10:21 -08:00
|
|
|
let base_convo_id = BaseConvoId::new(&seed_key);
|
|
|
|
|
let local_convo_id = base_convo_id.id_for_participant(Role::Responder);
|
|
|
|
|
let remote_convo_id = base_convo_id.id_for_participant(Role::Initiator);
|
|
|
|
|
|
2026-02-18 09:29:33 -08:00
|
|
|
// TODO: (P3) Rename; This accepts a Ephemeral key in most cases
|
|
|
|
|
let dh_self_installation_keypair =
|
|
|
|
|
InstallationKeyPair::from_secret_bytes(dh_self.DANGER_to_bytes());
|
|
|
|
|
// TODO: Danger - Fix double-ratchets types to Accept SymmetricKey32
|
|
|
|
|
let dr_state =
|
|
|
|
|
RatchetState::init_receiver(seed_key.DANGER_to_bytes(), dh_self_installation_keypair);
|
2026-02-11 14:10:21 -08:00
|
|
|
|
2026-02-04 06:17:45 +07:00
|
|
|
Self {
|
2026-02-11 14:10:21 -08:00
|
|
|
local_convo_id,
|
|
|
|
|
remote_convo_id,
|
|
|
|
|
dr_state,
|
2026-02-04 06:17:45 +07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn encrypt(&mut self, frame: PrivateV1Frame) -> EncryptedPayload {
|
|
|
|
|
let encoded_bytes = frame.encode_to_vec();
|
|
|
|
|
let (cipher_text, header) = self.dr_state.encrypt_message(&encoded_bytes);
|
2026-01-22 06:39:09 +07:00
|
|
|
|
|
|
|
|
EncryptedPayload {
|
|
|
|
|
encryption: Some(Encryption::Doubleratchet(Doubleratchet {
|
2026-02-04 06:17:45 +07:00
|
|
|
dh: Bytes::from(Vec::from(header.dh_pub.to_bytes())),
|
|
|
|
|
msg_num: header.msg_num,
|
|
|
|
|
prev_chain_len: header.prev_chain_len,
|
|
|
|
|
ciphertext: Bytes::from(cipher_text),
|
2026-01-22 06:39:09 +07:00
|
|
|
aux: "".into(),
|
|
|
|
|
})),
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-04 06:17:45 +07:00
|
|
|
|
|
|
|
|
fn decrypt(&mut self, payload: EncryptedPayload) -> Result<PrivateV1Frame, EncryptionError> {
|
|
|
|
|
// Validate and extract the encryption header or return errors
|
|
|
|
|
let dr_header = if let Some(enc) = payload.encryption {
|
|
|
|
|
if let proto::Encryption::Doubleratchet(dr) = enc {
|
|
|
|
|
dr
|
|
|
|
|
} else {
|
|
|
|
|
return Err(EncryptionError::Decryption(
|
|
|
|
|
"incorrect encryption type".into(),
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
return Err(EncryptionError::Decryption("missing payload".into()));
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Turn the bytes into a PublicKey
|
|
|
|
|
let byte_arr: [u8; 32] = dr_header
|
|
|
|
|
.dh
|
|
|
|
|
.to_vec()
|
|
|
|
|
.try_into()
|
|
|
|
|
.map_err(|_| EncryptionError::Decryption("invalid public key length".into()))?;
|
|
|
|
|
let dh_pub = PublicKey::from(byte_arr);
|
|
|
|
|
|
|
|
|
|
// Build the Header that DR impl expects
|
|
|
|
|
let header = Header {
|
2026-02-18 09:29:33 -08:00
|
|
|
dh_pub: *dh_pub,
|
2026-02-04 06:17:45 +07:00
|
|
|
msg_num: dr_header.msg_num,
|
|
|
|
|
prev_chain_len: dr_header.prev_chain_len,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Decrypt into Frame
|
|
|
|
|
let content_bytes = self
|
|
|
|
|
.dr_state
|
|
|
|
|
.decrypt_message(&dr_header.ciphertext, header)
|
|
|
|
|
.map_err(|e| EncryptionError::Decryption(e.to_string()))?;
|
|
|
|
|
Ok(PrivateV1Frame::decode(content_bytes.as_slice()).unwrap())
|
|
|
|
|
}
|
2026-02-06 23:41:12 +07:00
|
|
|
|
2026-04-10 08:33:58 +08:00
|
|
|
/// Persists a conversation's metadata and ratchet state to DB.
|
refactor(core): replace Rc-based Context with a synchronous, Send-able Core (#123)
Make the conversations core Send so the threaded client can own it behind an
Arc<Mutex<Core>>: a background worker polls the transport and handles inbound
payloads while the application thread issues outbound calls (send, create
conversation). Sharing the core across those two threads means moving it into
the spawned worker, which is only legal if it is Send. Access stays serialized
by the client's Mutex (one thread at a time), so the core needs Send but not
Sync and carries no lock of its own. See
docs/adr/0001-client-event-system.md for the background-poller design.
The Rc<RefCell> service-sharing is what made the core !Send. Context is de-Rc'd
and renamed to Core, owning its services outright and driving the inbox and
conversation primitives with plain &mut self.
- Services (identity, delivery, store, registry, MLS context, causal history)
are bundled into a ServiceContext<S> behind an ExternalServices trait, with
S = (DS, RS, CS). Constructors live on the (DS, RS, CS) form because S cannot
be inferred backwards through S::DS.
- Inbox, InboxV2, PrivateV1Convo, and GroupV1Convo become non-generic and
receive the ServiceContext bundle as a &mut/& parameter; no Rc or
RefCell-as-shared-state remains, so Core is Send whenever its injected
services are.
- Dispatch branches on ConversationKind in one place: Core rebuilds the target
as a Convo<S>/GroupConvo<S> trait object bound to the service bundle, so
conversations never escape the orchestrator.
- CausalHistoryStore drops its Rc, keeping a plain RefCell.
2026-06-08 21:55:33 +02:00
|
|
|
pub fn persist<S: ConversationStore + RatchetStore>(
|
|
|
|
|
&mut self,
|
|
|
|
|
store: &mut S,
|
|
|
|
|
) -> Result<ConversationId, ChatError> {
|
2026-04-10 08:33:58 +08:00
|
|
|
let convo_info = ConversationMeta {
|
|
|
|
|
local_convo_id: self.id().to_string(),
|
|
|
|
|
remote_convo_id: self.remote_id(),
|
|
|
|
|
kind: self.convo_type(),
|
|
|
|
|
};
|
refactor(core): replace Rc-based Context with a synchronous, Send-able Core (#123)
Make the conversations core Send so the threaded client can own it behind an
Arc<Mutex<Core>>: a background worker polls the transport and handles inbound
payloads while the application thread issues outbound calls (send, create
conversation). Sharing the core across those two threads means moving it into
the spawned worker, which is only legal if it is Send. Access stays serialized
by the client's Mutex (one thread at a time), so the core needs Send but not
Sync and carries no lock of its own. See
docs/adr/0001-client-event-system.md for the background-poller design.
The Rc<RefCell> service-sharing is what made the core !Send. Context is de-Rc'd
and renamed to Core, owning its services outright and driving the inbox and
conversation primitives with plain &mut self.
- Services (identity, delivery, store, registry, MLS context, causal history)
are bundled into a ServiceContext<S> behind an ExternalServices trait, with
S = (DS, RS, CS). Constructors live on the (DS, RS, CS) form because S cannot
be inferred backwards through S::DS.
- Inbox, InboxV2, PrivateV1Convo, and GroupV1Convo become non-generic and
receive the ServiceContext bundle as a &mut/& parameter; no Rc or
RefCell-as-shared-state remains, so Core is Send whenever its injected
services are.
- Dispatch branches on ConversationKind in one place: Core rebuilds the target
as a Convo<S>/GroupConvo<S> trait object bound to the service bundle, so
conversations never escape the orchestrator.
- CausalHistoryStore drops its Rc, keeping a plain RefCell.
2026-06-08 21:55:33 +02:00
|
|
|
store.save_conversation(&convo_info)?;
|
|
|
|
|
self.save_ratchet_state(store)?;
|
feat: introduce client event system (#106)
* chore(flake): accept extra system attr; add perl for openssl-sys build
forAllSystems calls the lambda with {system, pkgs}; strict
destructuring requires `..` to ignore the system attribute.
`pkgs.perl` is needed because openssl-sys is pulled vendored via
libsqlite3-sys / rusqlite / chat-sqlite, and its `perl Configure`
step needs FindBin.pm, which Fedora's system perl doesn't ship.
* feat: introduce client event system
- Core processing yields a `PayloadOutcome` enum — `Empty`, `Convo`, or
`Inbox`. `ConvoOutcome` carries a conversation id and an optional
decrypted `Content`; `InboxOutcome` adds a `NewConversation`
(id + `ConversationClass`) for a peer-initiated conversation.
- Client translates `PayloadOutcome` into app-facing `Vec<Event>`
(`ConversationStarted`, `MessageReceived`) at the boundary, so the
application loop sees discrete events rather than core types.
- MLS group welcomes produce a `ConversationStarted` event with no
initial content, fixing the silent-group-join case where the inbox
layer dropped the observation.
- C FFI exposes an `EventList` opaque type with indexed accessors and
an `Invalid` sentinel for out-of-bounds / non-applicable reads.
- Symmetric `Inbox` / `InboxV2` handlers: both return
`Result<InboxOutcome, _>` and own the persistence + ephemeral-key
cleanup for the conversations they create.
- Updated and simplified `docs/adr/0001-client-event-system.md`.
* chore(flake): bump nixpkgs to nixos-unstable-small
Temporary. The two crates.io UA fixes (NixOS/nixpkgs#512735 for
fetchCargoVendor's python-requests UA, NixOS/nixpkgs#524985 for
importCargoLock's curl UA) haven't propagated to nixos-unstable yet.
Switch to nixos-unstable-small and force logos-delivery to follow so
the smoketest gets the same fix. Revert once nixos-unstable catches up.
Refs:
- https://github.com/rust-lang/crates.io/issues/13482
- https://github.com/rust-lang/crates.io/issues/13783
- https://crates.io/data-access
2026-05-28 23:51:15 +02:00
|
|
|
Ok(self.id().to_string())
|
2026-04-10 08:33:58 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-03 08:25:26 +08:00
|
|
|
pub fn save_ratchet_state<T: RatchetStore>(&self, storage: &mut T) -> Result<(), ChatError> {
|
|
|
|
|
let record = to_ratchet_record(&self.dr_state);
|
|
|
|
|
let skipped_keys = to_skipped_key_records(&self.dr_state.skipped_keys());
|
|
|
|
|
storage.save_ratchet_state(&self.local_convo_id, &record, &skipped_keys)?;
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
feat: introduce client event system (#106)
* chore(flake): accept extra system attr; add perl for openssl-sys build
forAllSystems calls the lambda with {system, pkgs}; strict
destructuring requires `..` to ignore the system attribute.
`pkgs.perl` is needed because openssl-sys is pulled vendored via
libsqlite3-sys / rusqlite / chat-sqlite, and its `perl Configure`
step needs FindBin.pm, which Fedora's system perl doesn't ship.
* feat: introduce client event system
- Core processing yields a `PayloadOutcome` enum — `Empty`, `Convo`, or
`Inbox`. `ConvoOutcome` carries a conversation id and an optional
decrypted `Content`; `InboxOutcome` adds a `NewConversation`
(id + `ConversationClass`) for a peer-initiated conversation.
- Client translates `PayloadOutcome` into app-facing `Vec<Event>`
(`ConversationStarted`, `MessageReceived`) at the boundary, so the
application loop sees discrete events rather than core types.
- MLS group welcomes produce a `ConversationStarted` event with no
initial content, fixing the silent-group-join case where the inbox
layer dropped the observation.
- C FFI exposes an `EventList` opaque type with indexed accessors and
an `Invalid` sentinel for out-of-bounds / non-applicable reads.
- Symmetric `Inbox` / `InboxV2` handlers: both return
`Result<InboxOutcome, _>` and own the persistence + ephemeral-key
cleanup for the conversations they create.
- Updated and simplified `docs/adr/0001-client-event-system.md`.
* chore(flake): bump nixpkgs to nixos-unstable-small
Temporary. The two crates.io UA fixes (NixOS/nixpkgs#512735 for
fetchCargoVendor's python-requests UA, NixOS/nixpkgs#524985 for
importCargoLock's curl UA) haven't propagated to nixos-unstable yet.
Switch to nixos-unstable-small and force logos-delivery to follow so
the smoketest gets the same fix. Revert once nixos-unstable catches up.
Refs:
- https://github.com/rust-lang/crates.io/issues/13482
- https://github.com/rust-lang/crates.io/issues/13783
- https://crates.io/data-access
2026-05-28 23:51:15 +02:00
|
|
|
|
|
|
|
|
fn handle_content(&self, bytes: Bytes) -> Content {
|
|
|
|
|
Content {
|
|
|
|
|
bytes: bytes.into(),
|
2026-06-19 01:51:57 -07:00
|
|
|
encoded_credential: vec![114, 114],
|
feat: introduce client event system (#106)
* chore(flake): accept extra system attr; add perl for openssl-sys build
forAllSystems calls the lambda with {system, pkgs}; strict
destructuring requires `..` to ignore the system attribute.
`pkgs.perl` is needed because openssl-sys is pulled vendored via
libsqlite3-sys / rusqlite / chat-sqlite, and its `perl Configure`
step needs FindBin.pm, which Fedora's system perl doesn't ship.
* feat: introduce client event system
- Core processing yields a `PayloadOutcome` enum — `Empty`, `Convo`, or
`Inbox`. `ConvoOutcome` carries a conversation id and an optional
decrypted `Content`; `InboxOutcome` adds a `NewConversation`
(id + `ConversationClass`) for a peer-initiated conversation.
- Client translates `PayloadOutcome` into app-facing `Vec<Event>`
(`ConversationStarted`, `MessageReceived`) at the boundary, so the
application loop sees discrete events rather than core types.
- MLS group welcomes produce a `ConversationStarted` event with no
initial content, fixing the silent-group-join case where the inbox
layer dropped the observation.
- C FFI exposes an `EventList` opaque type with indexed accessors and
an `Invalid` sentinel for out-of-bounds / non-applicable reads.
- Symmetric `Inbox` / `InboxV2` handlers: both return
`Result<InboxOutcome, _>` and own the persistence + ephemeral-key
cleanup for the conversations they create.
- Updated and simplified `docs/adr/0001-client-event-system.md`.
* chore(flake): bump nixpkgs to nixos-unstable-small
Temporary. The two crates.io UA fixes (NixOS/nixpkgs#512735 for
fetchCargoVendor's python-requests UA, NixOS/nixpkgs#524985 for
importCargoLock's curl UA) haven't propagated to nixos-unstable yet.
Switch to nixos-unstable-small and force logos-delivery to follow so
the smoketest gets the same fix. Revert once nixos-unstable catches up.
Refs:
- https://github.com/rust-lang/crates.io/issues/13482
- https://github.com/rust-lang/crates.io/issues/13783
- https://crates.io/data-access
2026-05-28 23:51:15 +02:00
|
|
|
}
|
|
|
|
|
}
|
2025-12-22 09:40:46 -08:00
|
|
|
|
refactor(core): replace Rc-based Context with a synchronous, Send-able Core (#123)
Make the conversations core Send so the threaded client can own it behind an
Arc<Mutex<Core>>: a background worker polls the transport and handles inbound
payloads while the application thread issues outbound calls (send, create
conversation). Sharing the core across those two threads means moving it into
the spawned worker, which is only legal if it is Send. Access stays serialized
by the client's Mutex (one thread at a time), so the core needs Send but not
Sync and carries no lock of its own. See
docs/adr/0001-client-event-system.md for the background-poller design.
The Rc<RefCell> service-sharing is what made the core !Send. Context is de-Rc'd
and renamed to Core, owning its services outright and driving the inbox and
conversation primitives with plain &mut self.
- Services (identity, delivery, store, registry, MLS context, causal history)
are bundled into a ServiceContext<S> behind an ExternalServices trait, with
S = (DS, RS, CS). Constructors live on the (DS, RS, CS) form because S cannot
be inferred backwards through S::DS.
- Inbox, InboxV2, PrivateV1Convo, and GroupV1Convo become non-generic and
receive the ServiceContext bundle as a &mut/& parameter; no Rc or
RefCell-as-shared-state remains, so Core is Send whenever its injected
services are.
- Dispatch branches on ConversationKind in one place: Core rebuilds the target
as a Convo<S>/GroupConvo<S> trait object bound to the service bundle, so
conversations never escape the orchestrator.
- CausalHistoryStore drops its Rc, keeping a plain RefCell.
2026-06-08 21:55:33 +02:00
|
|
|
pub fn encrypt_content<S: RatchetStore>(
|
2026-01-29 23:36:18 +07:00
|
|
|
&mut self,
|
|
|
|
|
content: &[u8],
|
refactor(core): replace Rc-based Context with a synchronous, Send-able Core (#123)
Make the conversations core Send so the threaded client can own it behind an
Arc<Mutex<Core>>: a background worker polls the transport and handles inbound
payloads while the application thread issues outbound calls (send, create
conversation). Sharing the core across those two threads means moving it into
the spawned worker, which is only legal if it is Send. Access stays serialized
by the client's Mutex (one thread at a time), so the core needs Send but not
Sync and carries no lock of its own. See
docs/adr/0001-client-event-system.md for the background-poller design.
The Rc<RefCell> service-sharing is what made the core !Send. Context is de-Rc'd
and renamed to Core, owning its services outright and driving the inbox and
conversation primitives with plain &mut self.
- Services (identity, delivery, store, registry, MLS context, causal history)
are bundled into a ServiceContext<S> behind an ExternalServices trait, with
S = (DS, RS, CS). Constructors live on the (DS, RS, CS) form because S cannot
be inferred backwards through S::DS.
- Inbox, InboxV2, PrivateV1Convo, and GroupV1Convo become non-generic and
receive the ServiceContext bundle as a &mut/& parameter; no Rc or
RefCell-as-shared-state remains, so Core is Send whenever its injected
services are.
- Dispatch branches on ConversationKind in one place: Core rebuilds the target
as a Convo<S>/GroupConvo<S> trait object bound to the service bundle, so
conversations never escape the orchestrator.
- CausalHistoryStore drops its Rc, keeping a plain RefCell.
2026-06-08 21:55:33 +02:00
|
|
|
store: &mut S,
|
2026-01-29 23:36:18 +07:00
|
|
|
) -> Result<Vec<AddressedEncryptedPayload>, ChatError> {
|
2026-01-22 06:39:09 +07:00
|
|
|
let frame = PrivateV1Frame {
|
|
|
|
|
conversation_id: self.id().into(),
|
|
|
|
|
sender: "delete".into(),
|
|
|
|
|
timestamp: timestamp_millis(),
|
|
|
|
|
frame_type: Some(FrameType::Content(content.to_vec().into())),
|
|
|
|
|
};
|
|
|
|
|
|
2026-01-29 23:36:18 +07:00
|
|
|
let data = self.encrypt(frame);
|
2025-12-22 09:40:46 -08:00
|
|
|
|
refactor(core): replace Rc-based Context with a synchronous, Send-able Core (#123)
Make the conversations core Send so the threaded client can own it behind an
Arc<Mutex<Core>>: a background worker polls the transport and handles inbound
payloads while the application thread issues outbound calls (send, create
conversation). Sharing the core across those two threads means moving it into
the spawned worker, which is only legal if it is Send. Access stays serialized
by the client's Mutex (one thread at a time), so the core needs Send but not
Sync and carries no lock of its own. See
docs/adr/0001-client-event-system.md for the background-poller design.
The Rc<RefCell> service-sharing is what made the core !Send. Context is de-Rc'd
and renamed to Core, owning its services outright and driving the inbox and
conversation primitives with plain &mut self.
- Services (identity, delivery, store, registry, MLS context, causal history)
are bundled into a ServiceContext<S> behind an ExternalServices trait, with
S = (DS, RS, CS). Constructors live on the (DS, RS, CS) form because S cannot
be inferred backwards through S::DS.
- Inbox, InboxV2, PrivateV1Convo, and GroupV1Convo become non-generic and
receive the ServiceContext bundle as a &mut/& parameter; no Rc or
RefCell-as-shared-state remains, so Core is Send whenever its injected
services are.
- Dispatch branches on ConversationKind in one place: Core rebuilds the target
as a Convo<S>/GroupConvo<S> trait object bound to the service bundle, so
conversations never escape the orchestrator.
- CausalHistoryStore drops its Rc, keeping a plain RefCell.
2026-06-08 21:55:33 +02:00
|
|
|
self.save_ratchet_state(store)?;
|
2026-04-10 08:33:58 +08:00
|
|
|
|
2026-01-29 23:36:18 +07:00
|
|
|
Ok(vec![AddressedEncryptedPayload {
|
refactor(core): replace Rc-based Context with a synchronous, Send-able Core (#123)
Make the conversations core Send so the threaded client can own it behind an
Arc<Mutex<Core>>: a background worker polls the transport and handles inbound
payloads while the application thread issues outbound calls (send, create
conversation). Sharing the core across those two threads means moving it into
the spawned worker, which is only legal if it is Send. Access stays serialized
by the client's Mutex (one thread at a time), so the core needs Send but not
Sync and carries no lock of its own. See
docs/adr/0001-client-event-system.md for the background-poller design.
The Rc<RefCell> service-sharing is what made the core !Send. Context is de-Rc'd
and renamed to Core, owning its services outright and driving the inbox and
conversation primitives with plain &mut self.
- Services (identity, delivery, store, registry, MLS context, causal history)
are bundled into a ServiceContext<S> behind an ExternalServices trait, with
S = (DS, RS, CS). Constructors live on the (DS, RS, CS) form because S cannot
be inferred backwards through S::DS.
- Inbox, InboxV2, PrivateV1Convo, and GroupV1Convo become non-generic and
receive the ServiceContext bundle as a &mut/& parameter; no Rc or
RefCell-as-shared-state remains, so Core is Send whenever its injected
services are.
- Dispatch branches on ConversationKind in one place: Core rebuilds the target
as a Convo<S>/GroupConvo<S> trait object bound to the service bundle, so
conversations never escape the orchestrator.
- CausalHistoryStore drops its Rc, keeping a plain RefCell.
2026-06-08 21:55:33 +02:00
|
|
|
delivery_address: PRIVATE_V1_INBOX_ADDRESS.into(),
|
2026-01-29 23:36:18 +07:00
|
|
|
data,
|
|
|
|
|
}])
|
|
|
|
|
}
|
|
|
|
|
|
refactor(core): replace Rc-based Context with a synchronous, Send-able Core (#123)
Make the conversations core Send so the threaded client can own it behind an
Arc<Mutex<Core>>: a background worker polls the transport and handles inbound
payloads while the application thread issues outbound calls (send, create
conversation). Sharing the core across those two threads means moving it into
the spawned worker, which is only legal if it is Send. Access stays serialized
by the client's Mutex (one thread at a time), so the core needs Send but not
Sync and carries no lock of its own. See
docs/adr/0001-client-event-system.md for the background-poller design.
The Rc<RefCell> service-sharing is what made the core !Send. Context is de-Rc'd
and renamed to Core, owning its services outright and driving the inbox and
conversation primitives with plain &mut self.
- Services (identity, delivery, store, registry, MLS context, causal history)
are bundled into a ServiceContext<S> behind an ExternalServices trait, with
S = (DS, RS, CS). Constructors live on the (DS, RS, CS) form because S cannot
be inferred backwards through S::DS.
- Inbox, InboxV2, PrivateV1Convo, and GroupV1Convo become non-generic and
receive the ServiceContext bundle as a &mut/& parameter; no Rc or
RefCell-as-shared-state remains, so Core is Send whenever its injected
services are.
- Dispatch branches on ConversationKind in one place: Core rebuilds the target
as a Convo<S>/GroupConvo<S> trait object bound to the service bundle, so
conversations never escape the orchestrator.
- CausalHistoryStore drops its Rc, keeping a plain RefCell.
2026-06-08 21:55:33 +02:00
|
|
|
pub fn remote_id(&self) -> String {
|
|
|
|
|
self.remote_convo_id.clone()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn convo_type(&self) -> ConversationKind {
|
|
|
|
|
ConversationKind::PrivateV1
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-19 08:43:55 -07:00
|
|
|
impl Identified for PrivateV1Convo {
|
|
|
|
|
fn id(&self) -> ConversationIdRef<'_> {
|
|
|
|
|
&self.local_convo_id
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
refactor(core): replace Rc-based Context with a synchronous, Send-able Core (#123)
Make the conversations core Send so the threaded client can own it behind an
Arc<Mutex<Core>>: a background worker polls the transport and handles inbound
payloads while the application thread issues outbound calls (send, create
conversation). Sharing the core across those two threads means moving it into
the spawned worker, which is only legal if it is Send. Access stays serialized
by the client's Mutex (one thread at a time), so the core needs Send but not
Sync and carries no lock of its own. See
docs/adr/0001-client-event-system.md for the background-poller design.
The Rc<RefCell> service-sharing is what made the core !Send. Context is de-Rc'd
and renamed to Core, owning its services outright and driving the inbox and
conversation primitives with plain &mut self.
- Services (identity, delivery, store, registry, MLS context, causal history)
are bundled into a ServiceContext<S> behind an ExternalServices trait, with
S = (DS, RS, CS). Constructors live on the (DS, RS, CS) form because S cannot
be inferred backwards through S::DS.
- Inbox, InboxV2, PrivateV1Convo, and GroupV1Convo become non-generic and
receive the ServiceContext bundle as a &mut/& parameter; no Rc or
RefCell-as-shared-state remains, so Core is Send whenever its injected
services are.
- Dispatch branches on ConversationKind in one place: Core rebuilds the target
as a Convo<S>/GroupConvo<S> trait object bound to the service bundle, so
conversations never escape the orchestrator.
- CausalHistoryStore drops its Rc, keeping a plain RefCell.
2026-06-08 21:55:33 +02:00
|
|
|
impl<S: ExternalServices> Convo<S> for PrivateV1Convo {
|
|
|
|
|
fn send_content(
|
|
|
|
|
&mut self,
|
|
|
|
|
cx: &mut ServiceContext<S>,
|
|
|
|
|
content: &[u8],
|
|
|
|
|
) -> Result<(), ChatError> {
|
|
|
|
|
let payloads = self.encrypt_content(content, &mut cx.store)?;
|
|
|
|
|
let remote_id = self.remote_id();
|
|
|
|
|
for payload in payloads {
|
|
|
|
|
cx.ds
|
|
|
|
|
.publish(payload.into_envelope(remote_id.clone()))
|
|
|
|
|
.map_err(|e| ChatError::Delivery(e.to_string()))?;
|
|
|
|
|
}
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-06 23:41:12 +07:00
|
|
|
fn handle_frame(
|
|
|
|
|
&mut self,
|
refactor(core): replace Rc-based Context with a synchronous, Send-able Core (#123)
Make the conversations core Send so the threaded client can own it behind an
Arc<Mutex<Core>>: a background worker polls the transport and handles inbound
payloads while the application thread issues outbound calls (send, create
conversation). Sharing the core across those two threads means moving it into
the spawned worker, which is only legal if it is Send. Access stays serialized
by the client's Mutex (one thread at a time), so the core needs Send but not
Sync and carries no lock of its own. See
docs/adr/0001-client-event-system.md for the background-poller design.
The Rc<RefCell> service-sharing is what made the core !Send. Context is de-Rc'd
and renamed to Core, owning its services outright and driving the inbox and
conversation primitives with plain &mut self.
- Services (identity, delivery, store, registry, MLS context, causal history)
are bundled into a ServiceContext<S> behind an ExternalServices trait, with
S = (DS, RS, CS). Constructors live on the (DS, RS, CS) form because S cannot
be inferred backwards through S::DS.
- Inbox, InboxV2, PrivateV1Convo, and GroupV1Convo become non-generic and
receive the ServiceContext bundle as a &mut/& parameter; no Rc or
RefCell-as-shared-state remains, so Core is Send whenever its injected
services are.
- Dispatch branches on ConversationKind in one place: Core rebuilds the target
as a Convo<S>/GroupConvo<S> trait object bound to the service bundle, so
conversations never escape the orchestrator.
- CausalHistoryStore drops its Rc, keeping a plain RefCell.
2026-06-08 21:55:33 +02:00
|
|
|
cx: &mut ServiceContext<S>,
|
|
|
|
|
enc: EncryptedPayload,
|
feat: introduce client event system (#106)
* chore(flake): accept extra system attr; add perl for openssl-sys build
forAllSystems calls the lambda with {system, pkgs}; strict
destructuring requires `..` to ignore the system attribute.
`pkgs.perl` is needed because openssl-sys is pulled vendored via
libsqlite3-sys / rusqlite / chat-sqlite, and its `perl Configure`
step needs FindBin.pm, which Fedora's system perl doesn't ship.
* feat: introduce client event system
- Core processing yields a `PayloadOutcome` enum — `Empty`, `Convo`, or
`Inbox`. `ConvoOutcome` carries a conversation id and an optional
decrypted `Content`; `InboxOutcome` adds a `NewConversation`
(id + `ConversationClass`) for a peer-initiated conversation.
- Client translates `PayloadOutcome` into app-facing `Vec<Event>`
(`ConversationStarted`, `MessageReceived`) at the boundary, so the
application loop sees discrete events rather than core types.
- MLS group welcomes produce a `ConversationStarted` event with no
initial content, fixing the silent-group-join case where the inbox
layer dropped the observation.
- C FFI exposes an `EventList` opaque type with indexed accessors and
an `Invalid` sentinel for out-of-bounds / non-applicable reads.
- Symmetric `Inbox` / `InboxV2` handlers: both return
`Result<InboxOutcome, _>` and own the persistence + ephemeral-key
cleanup for the conversations they create.
- Updated and simplified `docs/adr/0001-client-event-system.md`.
* chore(flake): bump nixpkgs to nixos-unstable-small
Temporary. The two crates.io UA fixes (NixOS/nixpkgs#512735 for
fetchCargoVendor's python-requests UA, NixOS/nixpkgs#524985 for
importCargoLock's curl UA) haven't propagated to nixos-unstable yet.
Switch to nixos-unstable-small and force logos-delivery to follow so
the smoketest gets the same fix. Revert once nixos-unstable catches up.
Refs:
- https://github.com/rust-lang/crates.io/issues/13482
- https://github.com/rust-lang/crates.io/issues/13783
- https://crates.io/data-access
2026-05-28 23:51:15 +02:00
|
|
|
) -> Result<ConvoOutcome, ChatError> {
|
2026-02-06 23:41:12 +07:00
|
|
|
let frame = self
|
refactor(core): replace Rc-based Context with a synchronous, Send-able Core (#123)
Make the conversations core Send so the threaded client can own it behind an
Arc<Mutex<Core>>: a background worker polls the transport and handles inbound
payloads while the application thread issues outbound calls (send, create
conversation). Sharing the core across those two threads means moving it into
the spawned worker, which is only legal if it is Send. Access stays serialized
by the client's Mutex (one thread at a time), so the core needs Send but not
Sync and carries no lock of its own. See
docs/adr/0001-client-event-system.md for the background-poller design.
The Rc<RefCell> service-sharing is what made the core !Send. Context is de-Rc'd
and renamed to Core, owning its services outright and driving the inbox and
conversation primitives with plain &mut self.
- Services (identity, delivery, store, registry, MLS context, causal history)
are bundled into a ServiceContext<S> behind an ExternalServices trait, with
S = (DS, RS, CS). Constructors live on the (DS, RS, CS) form because S cannot
be inferred backwards through S::DS.
- Inbox, InboxV2, PrivateV1Convo, and GroupV1Convo become non-generic and
receive the ServiceContext bundle as a &mut/& parameter; no Rc or
RefCell-as-shared-state remains, so Core is Send whenever its injected
services are.
- Dispatch branches on ConversationKind in one place: Core rebuilds the target
as a Convo<S>/GroupConvo<S> trait object bound to the service bundle, so
conversations never escape the orchestrator.
- CausalHistoryStore drops its Rc, keeping a plain RefCell.
2026-06-08 21:55:33 +02:00
|
|
|
.decrypt(enc)
|
2026-02-06 23:41:12 +07:00
|
|
|
.map_err(|_| ChatError::Protocol("decryption".into()))?;
|
|
|
|
|
|
|
|
|
|
let Some(frame_type) = frame.frame_type else {
|
|
|
|
|
return Err(ChatError::ProtocolExpectation("None", "Some".into()));
|
|
|
|
|
};
|
|
|
|
|
|
refactor(core): replace Rc-based Context with a synchronous, Send-able Core (#123)
Make the conversations core Send so the threaded client can own it behind an
Arc<Mutex<Core>>: a background worker polls the transport and handles inbound
payloads while the application thread issues outbound calls (send, create
conversation). Sharing the core across those two threads means moving it into
the spawned worker, which is only legal if it is Send. Access stays serialized
by the client's Mutex (one thread at a time), so the core needs Send but not
Sync and carries no lock of its own. See
docs/adr/0001-client-event-system.md for the background-poller design.
The Rc<RefCell> service-sharing is what made the core !Send. Context is de-Rc'd
and renamed to Core, owning its services outright and driving the inbox and
conversation primitives with plain &mut self.
- Services (identity, delivery, store, registry, MLS context, causal history)
are bundled into a ServiceContext<S> behind an ExternalServices trait, with
S = (DS, RS, CS). Constructors live on the (DS, RS, CS) form because S cannot
be inferred backwards through S::DS.
- Inbox, InboxV2, PrivateV1Convo, and GroupV1Convo become non-generic and
receive the ServiceContext bundle as a &mut/& parameter; no Rc or
RefCell-as-shared-state remains, so Core is Send whenever its injected
services are.
- Dispatch branches on ConversationKind in one place: Core rebuilds the target
as a Convo<S>/GroupConvo<S> trait object bound to the service bundle, so
conversations never escape the orchestrator.
- CausalHistoryStore drops its Rc, keeping a plain RefCell.
2026-06-08 21:55:33 +02:00
|
|
|
self.save_ratchet_state(&mut cx.store)?;
|
2026-04-10 08:33:58 +08:00
|
|
|
|
feat: introduce client event system (#106)
* chore(flake): accept extra system attr; add perl for openssl-sys build
forAllSystems calls the lambda with {system, pkgs}; strict
destructuring requires `..` to ignore the system attribute.
`pkgs.perl` is needed because openssl-sys is pulled vendored via
libsqlite3-sys / rusqlite / chat-sqlite, and its `perl Configure`
step needs FindBin.pm, which Fedora's system perl doesn't ship.
* feat: introduce client event system
- Core processing yields a `PayloadOutcome` enum — `Empty`, `Convo`, or
`Inbox`. `ConvoOutcome` carries a conversation id and an optional
decrypted `Content`; `InboxOutcome` adds a `NewConversation`
(id + `ConversationClass`) for a peer-initiated conversation.
- Client translates `PayloadOutcome` into app-facing `Vec<Event>`
(`ConversationStarted`, `MessageReceived`) at the boundary, so the
application loop sees discrete events rather than core types.
- MLS group welcomes produce a `ConversationStarted` event with no
initial content, fixing the silent-group-join case where the inbox
layer dropped the observation.
- C FFI exposes an `EventList` opaque type with indexed accessors and
an `Invalid` sentinel for out-of-bounds / non-applicable reads.
- Symmetric `Inbox` / `InboxV2` handlers: both return
`Result<InboxOutcome, _>` and own the persistence + ephemeral-key
cleanup for the conversations they create.
- Updated and simplified `docs/adr/0001-client-event-system.md`.
* chore(flake): bump nixpkgs to nixos-unstable-small
Temporary. The two crates.io UA fixes (NixOS/nixpkgs#512735 for
fetchCargoVendor's python-requests UA, NixOS/nixpkgs#524985 for
importCargoLock's curl UA) haven't propagated to nixos-unstable yet.
Switch to nixos-unstable-small and force logos-delivery to follow so
the smoketest gets the same fix. Revert once nixos-unstable catches up.
Refs:
- https://github.com/rust-lang/crates.io/issues/13482
- https://github.com/rust-lang/crates.io/issues/13783
- https://crates.io/data-access
2026-05-28 23:51:15 +02:00
|
|
|
let content = match frame_type {
|
|
|
|
|
FrameType::Content(bytes) => Some(self.handle_content(bytes)),
|
2026-02-06 23:41:12 +07:00
|
|
|
FrameType::Placeholder(_) => None,
|
|
|
|
|
};
|
feat: introduce client event system (#106)
* chore(flake): accept extra system attr; add perl for openssl-sys build
forAllSystems calls the lambda with {system, pkgs}; strict
destructuring requires `..` to ignore the system attribute.
`pkgs.perl` is needed because openssl-sys is pulled vendored via
libsqlite3-sys / rusqlite / chat-sqlite, and its `perl Configure`
step needs FindBin.pm, which Fedora's system perl doesn't ship.
* feat: introduce client event system
- Core processing yields a `PayloadOutcome` enum — `Empty`, `Convo`, or
`Inbox`. `ConvoOutcome` carries a conversation id and an optional
decrypted `Content`; `InboxOutcome` adds a `NewConversation`
(id + `ConversationClass`) for a peer-initiated conversation.
- Client translates `PayloadOutcome` into app-facing `Vec<Event>`
(`ConversationStarted`, `MessageReceived`) at the boundary, so the
application loop sees discrete events rather than core types.
- MLS group welcomes produce a `ConversationStarted` event with no
initial content, fixing the silent-group-join case where the inbox
layer dropped the observation.
- C FFI exposes an `EventList` opaque type with indexed accessors and
an `Invalid` sentinel for out-of-bounds / non-applicable reads.
- Symmetric `Inbox` / `InboxV2` handlers: both return
`Result<InboxOutcome, _>` and own the persistence + ephemeral-key
cleanup for the conversations they create.
- Updated and simplified `docs/adr/0001-client-event-system.md`.
* chore(flake): bump nixpkgs to nixos-unstable-small
Temporary. The two crates.io UA fixes (NixOS/nixpkgs#512735 for
fetchCargoVendor's python-requests UA, NixOS/nixpkgs#524985 for
importCargoLock's curl UA) haven't propagated to nixos-unstable yet.
Switch to nixos-unstable-small and force logos-delivery to follow so
the smoketest gets the same fix. Revert once nixos-unstable catches up.
Refs:
- https://github.com/rust-lang/crates.io/issues/13482
- https://github.com/rust-lang/crates.io/issues/13783
- https://crates.io/data-access
2026-05-28 23:51:15 +02:00
|
|
|
Ok(ConvoOutcome {
|
|
|
|
|
convo_id: self.id().to_string(),
|
|
|
|
|
content,
|
|
|
|
|
})
|
2026-02-06 23:41:12 +07:00
|
|
|
}
|
2026-06-15 13:15:18 -07:00
|
|
|
|
|
|
|
|
fn wakeup(&mut self, _: &mut ServiceContext<S>) -> Result<(), ChatError> {
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
2025-12-22 09:40:46 -08:00
|
|
|
}
|
2026-02-04 06:17:45 +07:00
|
|
|
|
refactor(core): replace Rc-based Context with a synchronous, Send-able Core (#123)
Make the conversations core Send so the threaded client can own it behind an
Arc<Mutex<Core>>: a background worker polls the transport and handles inbound
payloads while the application thread issues outbound calls (send, create
conversation). Sharing the core across those two threads means moving it into
the spawned worker, which is only legal if it is Send. Access stays serialized
by the client's Mutex (one thread at a time), so the core needs Send but not
Sync and carries no lock of its own. See
docs/adr/0001-client-event-system.md for the background-poller design.
The Rc<RefCell> service-sharing is what made the core !Send. Context is de-Rc'd
and renamed to Core, owning its services outright and driving the inbox and
conversation primitives with plain &mut self.
- Services (identity, delivery, store, registry, MLS context, causal history)
are bundled into a ServiceContext<S> behind an ExternalServices trait, with
S = (DS, RS, CS). Constructors live on the (DS, RS, CS) form because S cannot
be inferred backwards through S::DS.
- Inbox, InboxV2, PrivateV1Convo, and GroupV1Convo become non-generic and
receive the ServiceContext bundle as a &mut/& parameter; no Rc or
RefCell-as-shared-state remains, so Core is Send whenever its injected
services are.
- Dispatch branches on ConversationKind in one place: Core rebuilds the target
as a Convo<S>/GroupConvo<S> trait object bound to the service bundle, so
conversations never escape the orchestrator.
- CausalHistoryStore drops its Rc, keeping a plain RefCell.
2026-06-08 21:55:33 +02:00
|
|
|
impl Debug for PrivateV1Convo {
|
2026-02-04 06:17:45 +07:00
|
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
|
|
|
f.debug_struct("PrivateV1Convo")
|
|
|
|
|
.field("dr_state", &"******")
|
|
|
|
|
.finish()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
2026-02-18 09:29:33 -08:00
|
|
|
use crypto::PrivateKey;
|
2026-02-04 06:17:45 +07:00
|
|
|
|
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn test_encrypt_roundtrip() {
|
2026-02-18 09:29:33 -08:00
|
|
|
let saro = PrivateKey::random();
|
|
|
|
|
let raya = PrivateKey::random();
|
2026-02-04 06:17:45 +07:00
|
|
|
|
|
|
|
|
let pub_raya = PublicKey::from(&raya);
|
|
|
|
|
|
2026-02-18 09:29:33 -08:00
|
|
|
let seed_key = saro.diffie_hellman(&pub_raya).DANGER_to_bytes();
|
|
|
|
|
let seed_key_saro = SymmetricKey32::from(seed_key);
|
|
|
|
|
let seed_key_raya = SymmetricKey32::from(seed_key);
|
2026-02-04 06:17:45 +07:00
|
|
|
let send_content_bytes = vec![0, 2, 4, 6, 8];
|
refactor(core): replace Rc-based Context with a synchronous, Send-able Core (#123)
Make the conversations core Send so the threaded client can own it behind an
Arc<Mutex<Core>>: a background worker polls the transport and handles inbound
payloads while the application thread issues outbound calls (send, create
conversation). Sharing the core across those two threads means moving it into
the spawned worker, which is only legal if it is Send. Access stays serialized
by the client's Mutex (one thread at a time), so the core needs Send but not
Sync and carries no lock of its own. See
docs/adr/0001-client-event-system.md for the background-poller design.
The Rc<RefCell> service-sharing is what made the core !Send. Context is de-Rc'd
and renamed to Core, owning its services outright and driving the inbox and
conversation primitives with plain &mut self.
- Services (identity, delivery, store, registry, MLS context, causal history)
are bundled into a ServiceContext<S> behind an ExternalServices trait, with
S = (DS, RS, CS). Constructors live on the (DS, RS, CS) form because S cannot
be inferred backwards through S::DS.
- Inbox, InboxV2, PrivateV1Convo, and GroupV1Convo become non-generic and
receive the ServiceContext bundle as a &mut/& parameter; no Rc or
RefCell-as-shared-state remains, so Core is Send whenever its injected
services are.
- Dispatch branches on ConversationKind in one place: Core rebuilds the target
as a Convo<S>/GroupConvo<S> trait object bound to the service bundle, so
conversations never escape the orchestrator.
- CausalHistoryStore drops its Rc, keeping a plain RefCell.
2026-06-08 21:55:33 +02:00
|
|
|
let mut sr_convo = PrivateV1Convo::new_initiator(seed_key_saro, pub_raya);
|
|
|
|
|
let mut rs_convo = PrivateV1Convo::new_responder(seed_key_raya, &raya);
|
2026-02-04 06:17:45 +07:00
|
|
|
|
|
|
|
|
let send_frame = PrivateV1Frame {
|
|
|
|
|
conversation_id: "_".into(),
|
|
|
|
|
sender: Bytes::new(),
|
|
|
|
|
timestamp: timestamp_millis(),
|
|
|
|
|
frame_type: Some(FrameType::Content(Bytes::from(send_content_bytes.clone()))),
|
|
|
|
|
};
|
|
|
|
|
let payload = sr_convo.encrypt(send_frame.clone());
|
|
|
|
|
let recv_frame = rs_convo.decrypt(payload).unwrap();
|
|
|
|
|
|
|
|
|
|
assert!(
|
|
|
|
|
recv_frame == send_frame,
|
|
|
|
|
"{:?}. {:?}",
|
|
|
|
|
recv_frame,
|
|
|
|
|
send_content_bytes
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|