//! Single source of truth for `user_config` keys and their sync posture. //! //! BalancedBreakfast used to sync every `user_config` key unconditionally: the //! table's triggers enqueued a changelog row for any key, and the initial //! snapshot swept the whole table. That is the same shape audiofiles was //! hardened away from (fuzz-2026-07-06 #2, -07-20 #1, -07-21 #3): a config key //! that names device state must never cross the sync boundary, and "sync //! everything" cannot hold one back. //! //! [`CONFIG`] is the family's shared [`ConfigSpec`] for BB's `user_config` //! table, declaring each known key's [`Posture`]. It drives two filters: //! //! - the SQL export filter (the `config_key_policy` table, seeded from this spec //! by [`Database::seed_config_key_policy`](crate::Database::seed_config_key_policy) //! and joined by the `user_config` triggers and the initial snapshot), and //! - the Rust import filter ([`key_excluded_from_sync`]), applied when a remote //! pull batch is written back. //! //! Storage stays sqlx (BB's `ConfigRepository`), not `synckit_config::ConfigStore` //! (rusqlite): this crate takes only the pure-data half of synckit-config, the //! spec and posture vocabulary, so a sqlx app shares the declaration without the //! rusqlite store. Posture is fail-closed: an undeclared key is //! [`Local`](Posture::Local) and never syncs. //! //! use synckit_config::{ConfigSpec, Posture}; /// BB's `user_config` posture declaration. /// /// Only `theme` crosses the sync boundary. `bb-welcomed` is a per-device /// first-run flag (each device shows the welcome once). `bb-theme` is the legacy /// theme key read once to migrate the old value into `theme`; it must not sync /// on its own. Every other key the generic `set_config` command might write is /// undeclared and therefore `Local` (fail-closed). pub const CONFIG: ConfigSpec = ConfigSpec::new( "user_config", &[ // Synced: user preferences carried across the user's devices. ("theme", Posture::Synced), // Local: per-device state and the legacy alias, never synced. ("bb-welcomed", Posture::Local), ("bb-theme", Posture::Local), ], ); /// Import-side filter: true when a raw remote `user_config` key must be dropped /// rather than written back. A key the spec does not clear to sync is refused, /// which by the spec's fail-closed default also refuses any unknown key: a /// hostile server cannot set a device-local or unrecognized key by sending it. #[must_use] pub fn key_excluded_from_sync(key: &str) -> bool { !CONFIG.is_synced(key) } #[cfg(test)] mod tests { use super::*; #[test] fn only_theme_crosses_the_boundary() { assert!(CONFIG.is_synced("theme"), "theme is a shared preference"); assert!(!CONFIG.is_synced("bb-welcomed"), "welcome is per-device"); assert!( !CONFIG.is_synced("bb-theme"), "the legacy alias never syncs" ); } #[test] fn the_import_filter_refuses_local_and_unknown_keys() { assert!(!key_excluded_from_sync("theme"), "theme is admitted"); assert!( key_excluded_from_sync("bb-welcomed"), "per-device stays home" ); assert!( key_excluded_from_sync("totally_unknown"), "an unknown key fails closed", ); } // The policy rows the export filter is seeded from must carry every declared // key, marking only `theme` replicated. If this drifted, the SQL export and // the Rust import would disagree. #[test] fn policy_rows_replicate_only_theme() { let replicated: Vec<&str> = CONFIG .policy_rows() .filter(|row| row.replicated) .map(|row| row.key) .collect(); assert_eq!(replicated, ["theme"]); } }