Skip to main content

max / synckit

correct stale doc comments across client, store, keystore and config
Author: Max Johnson <me@maxj.phd> · 2026-08-19 18:12 UTC
Signed with PGP, not checked
Commit: 7695a7121647a05e4a1b11ce527d293281a57ce4
Parent: f65e9b3
9 files changed, +59 insertions, -46 deletions
@@ -7,7 +7,7 @@
7 7 //! This module provides:
8 8 //! - [`detect_conflicts`]: pure function that splits pulled changes into clean
9 9 //! (non-conflicting) and conflicting sets
10 - //! - [`resolve_lww`]: last-write-wins resolution by `client_timestamp`
10 + //! - [`resolve_lww`]: last-write-wins resolution by hybrid logical clock
11 11 //! - [`resolve_field_merge`]: 3-way JSON object merge using a base version
12 12 //! - [`ConflictResolver`]: trait for custom resolution strategies
13 13
@@ -248,8 +248,8 @@
248 248 ///
249 249 /// This replaces the prior wall-clock + "delete always wins" rule. Delete-wins
250 250 /// caused permanent loss when a delete on one device beat a strictly-newer edit on
251 - /// another (ultra-fuzz Run #28); under HLC ordering a newer edit beats an older
252 - /// delete and vice-versa, by clock value alone.
251 + /// another; under HLC ordering a newer edit beats an older delete and vice-versa,
252 + /// by clock value alone.
253 253 ///
254 254 /// An *exactly* equal HLC normally means a same-device echo, since a distinct
255 255 /// `node` makes the value unique. It can only collide across devices if two
@@ -2,34 +2,20 @@
2 2 //!
3 3 //! Feature-gated behind `keychain` (enabled by default).
4 4 //!
5 - //! The keychain is a *cache*, never the system of record: the master key is
6 - //! always recoverable from the server envelope with the user's password. So a
7 - //! keychain that is missing, locked, or absent entirely costs a password
8 - //! re-entry, not access to the data. Callers reflect
9 - //! that: see [`cache_key`], which reports failure rather than propagating it.
10 - //!
11 - //! ## Platform backends
12 - //!
13 - //! - **macOS**: legacy Keychain (Security framework), via `keyring`'s `v1` store.
14 - //! - **iOS**: Protected Data store (`kSecClassGenericPassword`), installed by
15 - //! this module. `keyring`'s `v1` helper cannot do it (its store installer is
16 - //! cfg'd out on iOS), so [`entry`] calls `keyring_core::set_default_store`
17 - //! directly with `apple_native_keyring_store::protected::Store`. Entries are
18 - //! created with an explicit [`IOS_ACCESS_POLICY`] rather than the store
19 - //! default; see that constant for why.
20 - //! - **Linux**: secret-service (D-Bus). Requires a running keyring daemon such
21 - //! as gnome-keyring. Without a secret-service provider, entry construction
22 - //! fails and the cache is simply unavailable.
23 - //! - **Windows**: Credential Manager.
5 + //! The per-platform backends, the cache-not-system-of-record rule, and the iOS
6 + //! accessibility-class rationale are in `docs/architecture.md` ("Keychain
7 + //! integration"). Two consequences show up in this file: [`cache_key`] reports
8 + //! failure rather than propagating it, and [`entry`] installs the iOS store
9 + //! itself with an explicit `IOS_ACCESS_POLICY`.
24 10
25 11 use crate::error::Result;
26 12 // Only the keychain code paths (and the test module) construct SyncKitError
27 13 // directly; without the feature a plain lib build would see it as unused.
28 14 #[cfg(any(feature = "keychain", test))]
29 15 use crate::error::SyncKitError;
16 + use crate::ids::{AppId, UserId};
30 17 // Only the keychain code paths use this top-level alias; the test module imports
31 18 // its own `B64` locally, so a no-keychain test build must not pull it in here.
32 - use crate::ids::{AppId, UserId};
33 19 #[cfg(feature = "keychain")]
34 20 use base64::{Engine, engine::general_purpose::STANDARD as B64};
35 21 #[cfg(test)]
@@ -50,9 +36,6 @@
50 36 }
51 37
52 38 /// Build the keychain user key: the `user_id` as a hyphenated UUID string.
53 - ///
54 - /// Combined with `service_name`, this uniquely identifies the keychain entry
55 - /// for a given (app, user) pair.
56 39 #[cfg(any(feature = "keychain", test))]
57 40 fn user_key(user_id: UserId) -> String {
58 41 user_id.to_string()
@@ -7,6 +7,8 @@
7 7 //! `SyncTable`, but nothing here depends on it, so a no-sync consumer (the Alloy
8 8 //! console remembering a theme) links this crate and not the sync runtime.
9 9 //!
10 + //! <!-- wiki: synckit-config-design -->
11 + //!
10 12 //! ## Why this is its own crate, and why it is SQLite
11 13 //!
12 14 //! Four apps stored settings four ways (a synced SQLite table, an
@@ -3,7 +3,7 @@
3 3 //! A [`ConfigSpec`] is the source of truth for postures: a static list of the
4 4 //! keys an app knows, each tagged [`Synced`] or [`Local`]. It is pure data with
5 5 //! no connection and no I/O, so it can be a `const` an app declares once. The
6 - //! sync side ([`synckit-client`]) reads [`policy_rows`](ConfigSpec::policy_rows)
6 + //! sync side (`synckit-client`) reads [`policy_rows`](ConfigSpec::policy_rows)
7 7 //! to seed the filter that keeps `Local` keys off the wire.
8 8 //!
9 9 //! [`Synced`]: Posture::Synced
@@ -70,7 +70,6 @@
70 70 // on next cold launch, not the setup.
71 71 keystore::cache_key(app_id, user_id, &master_key.0);
72 72
73 - // Store in memory
74 73 *self.master_key.write() = Some(master_key);
75 74
76 75 tracing::info!("New master key generated and stored");
@@ -92,7 +91,6 @@
92 91 // server envelope and can come from it again.
93 92 keystore::cache_key(app_id, user_id, &master_key.0);
94 93
95 - // Store in memory
96 94 *self.master_key.write() = Some(master_key);
97 95
98 96 tracing::info!("Master key recovered from server");
@@ -25,13 +25,38 @@
25 25 //! [`blob_confirm`](SyncKitClient::blob_confirm),
26 26 //! [`blob_download_url`](SyncKitClient::blob_download_url),
27 27 //! [`blob_download`](SyncKitClient::blob_download).
28 + //! - **Groups**: [`create_group`](SyncKitClient::create_group),
29 + //! [`list_groups`](SyncKitClient::list_groups),
30 + //! [`add_member`](SyncKitClient::add_member),
31 + //! [`remove_member`](SyncKitClient::remove_member),
32 + //! [`rotate_group_key`](SyncKitClient::rotate_group_key),
33 + //! [`group_push`](SyncKitClient::group_push),
34 + //! [`group_pull_rich`](SyncKitClient::group_pull_rich), and the invitation
35 + //! calls ([`create_invitation`](SyncKitClient::create_invitation),
36 + //! [`accept_invitation`](SyncKitClient::accept_invitation)).
37 + //! - **Key rotation**: [`rotate_key`](SyncKitClient::rotate_key).
38 + //! - **Change notifications**: [`subscribe`](SyncKitClient::subscribe), an SSE
39 + //! stream of [`SyncNotifyStream`].
40 + //! - **Billing**: [`get_app_pricing`](SyncKitClient::get_app_pricing),
41 + //! [`quote_price`](SyncKitClient::quote_price),
42 + //! [`get_subscription_status`](SyncKitClient::get_subscription_status),
43 + //! [`create_subscription_checkout`](SyncKitClient::create_subscription_checkout),
44 + //! [`queue_storage_cap_change`](SyncKitClient::queue_storage_cap_change).
45 + //! - **OTA publishing**: [`ota_create_release`](SyncKitClient::ota_create_release),
46 + //! [`ota_register_artifact`](SyncKitClient::ota_register_artifact),
47 + //! [`ota_upload_artifact`](SyncKitClient::ota_upload_artifact),
48 + //! [`ota_confirm_artifact`](SyncKitClient::ota_confirm_artifact),
49 + //! [`ota_updater_check`](SyncKitClient::ota_updater_check).
28 50 //!
29 51 //! ## Internal state
30 52 //!
31 - //! The client holds two `RwLock`-wrapped fields: the authenticated session
32 - //! (JWT token, user ID, app ID) and the 256-bit master encryption key. Both
33 - //! start as `None` and are populated by the authentication and encryption
34 - //! setup methods respectively.
53 + //! The client holds six `RwLock`-wrapped fields: the authenticated session
54 + //! (JWT token, user ID, app ID), the 256-bit master encryption key, the
55 + //! `key_id` that key was issued under, the pending rotation key, the decrypted
56 + //! Group Content Key cache, and the optional blob-resume store. The session and
57 + //! master key start as `None` and are populated by the authentication and
58 + //! encryption setup methods respectively; the rotation and group fields carry
59 + //! key material with their own lifecycles, documented on the struct.
35 60 //!
36 61 //! ## Thread safety
37 62 //!
@@ -41,9 +66,13 @@
41 66 //!
42 67 //! ## Retry strategy
43 68 //!
44 - //! All HTTP operations retry transient failures (network errors, 5xx,
45 - //! 429) up to 3 times with exponential backoff (1s, 2s, 4s). Client errors
46 - //! (4xx except 429) are permanent and returned immediately.
69 + //! Retries apply only to calls the call site marks idempotent-safe. Those
70 + //! retry transient failures (network errors, 5xx, 429) up to 3 times with
71 + //! exponential backoff (1s, 2s, 4s) spread by +/-20% jitter; a `Retry-After`
72 + //! header on a 429 overrides the computed delay, capped at 60 seconds. Calls
73 + //! marked unsafe to replay (Stripe checkout creation, for one) are attempted
74 + //! exactly once. Client errors (4xx except 429) are permanent and returned
75 + //! immediately.
47 76 //!
48 77 //! ## Token handling
49 78 //!
@@ -1,6 +1,6 @@
1 1 //! The sync half of portable config: a [`ConfigSpec`] as a [`SyncTable`].
2 2 //!
3 - //! [`synckit-config`] holds the local store an app reads and writes and the
3 + //! `synckit-config` holds the local store an app reads and writes and the
4 4 //! posture declaration; this turns that declaration into the sync engine's
5 5 //! terms. An app calls [`config_sync_table`] to fold config into its
6 6 //! [`SyncSchema`] and [`install_policy`] once at open to seed the filter the
@@ -4,14 +4,15 @@
4 4 //! hand-writes, the changelog schema and triggers, push/pull loops, FK-ordered
5 5 //! apply, HLC, blobs, and the scheduler, behind a declarative [`SyncSchema`].
6 6 //! An app declares its tables and per-table policy once; the engine owns the
7 - //! mechanics. See `docs/architecture.md` ("Proposed: the `SyncStore` helper").
7 + //! mechanics. See `docs/architecture.md` ("The `SyncStore` higher-level
8 + //! helper").
8 9 //!
9 - //! Build status: **complete (B1–B7)**, schema types ([`schema`]), trigger/
10 - //! migration generation ([`migrate`]), the DB seam + bookkeeping state ([`db`]),
11 - //! the FK-ordered apply engine ([`apply`]), the HLC ledger + conflict dispatch
12 - //! ([`hlc`]), the push/pull loops + lifecycle ([`sync`]), the blob engine
13 - //! ([`blob`]), and the [`SyncStore`](facade::SyncStore) facade + scheduler
14 - //! ([`facade`], [`scheduler`]).
10 + //! The modules: schema types ([`schema`]), trigger/migration generation
11 + //! ([`migrate`]), the DB seam + bookkeeping state ([`db`]), the FK-ordered apply
12 + //! engine ([`apply`]), the HLC ledger + conflict dispatch ([`hlc`]), the
13 + //! push/pull loops + lifecycle ([`sync`]), the blob engine ([`blob`]), and the
14 + //! [`SyncStore`](facade::SyncStore) facade + scheduler ([`facade`],
15 + //! [`scheduler`]).
15 16 //!
16 17 //! Start at [`SyncStore`](facade::SyncStore): declare a [`SyncSchema`](schema),
17 18 //! build a store, and call `sync_now` or `spawn_scheduler`.
@@ -8,8 +8,8 @@
8 8 //! trigger/snapshot/whitelist triplication (and its silent-drift bug class)
9 9 //! cannot recur.
10 10 //!
11 - //! See `docs/architecture.md` ("Proposed: the `SyncStore` helper") for the full
12 - //! design and worked GoingsOn / audiofiles / Balanced Breakfast manifests.
11 + //! See `docs/architecture.md` ("The `SyncStore` higher-level helper") for the
12 + //! full design and worked GoingsOn / audiofiles / Balanced Breakfast manifests.
13 13
14 14 /// How the engine resolves concurrent edits to the same row.
15 15 #[derive(Debug, Clone, Copy, PartialEq, Eq)]