feat(core): add VaultSettings with retention + generator + caps
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -53,6 +53,12 @@ pub use attachment::{AttachmentRef, AttachmentSummary};
|
|||||||
pub mod manifest;
|
pub mod manifest;
|
||||||
pub use manifest::{Manifest, ManifestEntry, MANIFEST_SCHEMA_VERSION};
|
pub use manifest::{Manifest, ManifestEntry, MANIFEST_SCHEMA_VERSION};
|
||||||
|
|
||||||
|
pub mod settings;
|
||||||
|
pub use settings::{
|
||||||
|
AttachmentCaps, Capitalization, CharClasses, GeneratorRequest, HistoryRetention,
|
||||||
|
SymbolCharset, TrashRetention, VaultSettings,
|
||||||
|
};
|
||||||
|
|
||||||
pub mod crypto;
|
pub mod crypto;
|
||||||
pub use crypto::{decrypt, derive_master_key, encrypt, KdfParams};
|
pub use crypto::{decrypt, derive_master_key, encrypt, KdfParams};
|
||||||
|
|
||||||
|
|||||||
173
crates/idfoto-core/src/settings.rs
Normal file
173
crates/idfoto-core/src/settings.rs
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
//! Vault-level settings: trash retention, history retention, generator
|
||||||
|
//! defaults, attachment caps, autofill TOFU acks.
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct VaultSettings {
|
||||||
|
pub trash_retention: TrashRetention,
|
||||||
|
pub field_history_retention: HistoryRetention,
|
||||||
|
pub generator_defaults: GeneratorRequest,
|
||||||
|
pub attachment_caps: AttachmentCaps,
|
||||||
|
/// hostname → unix-seconds first-acked
|
||||||
|
#[serde(default)]
|
||||||
|
pub autofill_origin_acks: HashMap<String, i64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for VaultSettings {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
trash_retention: TrashRetention::Days(30),
|
||||||
|
field_history_retention: HistoryRetention::Forever,
|
||||||
|
generator_defaults: GeneratorRequest::default(),
|
||||||
|
attachment_caps: AttachmentCaps::default(),
|
||||||
|
autofill_origin_acks: HashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
|
||||||
|
pub enum TrashRetention {
|
||||||
|
Days(u32),
|
||||||
|
Forever,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TrashRetention {
|
||||||
|
pub fn should_purge(&self, trashed_at: i64, now: i64) -> bool {
|
||||||
|
match self {
|
||||||
|
TrashRetention::Forever => false,
|
||||||
|
TrashRetention::Days(d) => now - trashed_at > (*d as i64) * 86_400,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
|
||||||
|
pub enum HistoryRetention {
|
||||||
|
LastN(u32),
|
||||||
|
Days(u32),
|
||||||
|
Forever,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(tag = "kind", rename_all = "snake_case")]
|
||||||
|
pub enum GeneratorRequest {
|
||||||
|
Bip39 {
|
||||||
|
word_count: u32,
|
||||||
|
separator: String,
|
||||||
|
capitalization: Capitalization,
|
||||||
|
},
|
||||||
|
Random {
|
||||||
|
length: u32,
|
||||||
|
classes: CharClasses,
|
||||||
|
symbol_charset: SymbolCharset,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for GeneratorRequest {
|
||||||
|
fn default() -> Self {
|
||||||
|
GeneratorRequest::Random {
|
||||||
|
length: 20,
|
||||||
|
classes: CharClasses { lower: true, upper: true, digits: true, symbols: true },
|
||||||
|
symbol_charset: SymbolCharset::SafeOnly,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum Capitalization {
|
||||||
|
Lower,
|
||||||
|
Upper,
|
||||||
|
FirstOfEach,
|
||||||
|
Title,
|
||||||
|
Mixed,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct CharClasses {
|
||||||
|
pub lower: bool,
|
||||||
|
pub upper: bool,
|
||||||
|
pub digits: bool,
|
||||||
|
pub symbols: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(tag = "kind", rename_all = "snake_case")]
|
||||||
|
pub enum SymbolCharset {
|
||||||
|
SafeOnly,
|
||||||
|
Extended,
|
||||||
|
Custom(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
||||||
|
pub struct AttachmentCaps {
|
||||||
|
pub per_attachment_max_bytes: u64,
|
||||||
|
pub per_item_max_count: u32,
|
||||||
|
pub per_vault_soft_cap_bytes: u64,
|
||||||
|
pub per_vault_hard_cap_bytes: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for AttachmentCaps {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
per_attachment_max_bytes: 10 * 1024 * 1024,
|
||||||
|
per_item_max_count: 20,
|
||||||
|
per_vault_soft_cap_bytes: 100 * 1024 * 1024,
|
||||||
|
per_vault_hard_cap_bytes: 500 * 1024 * 1024,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn defaults_match_spec() {
|
||||||
|
let s = VaultSettings::default();
|
||||||
|
assert!(matches!(s.trash_retention, TrashRetention::Days(30)));
|
||||||
|
assert!(matches!(s.field_history_retention, HistoryRetention::Forever));
|
||||||
|
assert_eq!(s.attachment_caps.per_attachment_max_bytes, 10 * 1024 * 1024);
|
||||||
|
assert_eq!(s.attachment_caps.per_item_max_count, 20);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn trash_retention_purges_after_days() {
|
||||||
|
let r = TrashRetention::Days(30);
|
||||||
|
let now = 1_000_000_000;
|
||||||
|
let recently_trashed = now - 29 * 86_400;
|
||||||
|
let long_trashed = now - 31 * 86_400;
|
||||||
|
assert!(!r.should_purge(recently_trashed, now));
|
||||||
|
assert!(r.should_purge(long_trashed, now));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn trash_retention_forever_never_purges() {
|
||||||
|
let r = TrashRetention::Forever;
|
||||||
|
assert!(!r.should_purge(0, 1_000_000_000));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn settings_round_trip() {
|
||||||
|
let s = VaultSettings::default();
|
||||||
|
let json = serde_json::to_string(&s).unwrap();
|
||||||
|
let parsed: VaultSettings = serde_json::from_str(&json).unwrap();
|
||||||
|
assert_eq!(parsed.attachment_caps.per_attachment_max_bytes,
|
||||||
|
s.attachment_caps.per_attachment_max_bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn random_generator_default_is_20_safe() {
|
||||||
|
match VaultSettings::default().generator_defaults {
|
||||||
|
GeneratorRequest::Random { length, classes, symbol_charset } => {
|
||||||
|
assert_eq!(length, 20);
|
||||||
|
assert!(classes.lower && classes.upper && classes.digits && classes.symbols);
|
||||||
|
assert!(matches!(symbol_charset, SymbolCharset::SafeOnly));
|
||||||
|
}
|
||||||
|
_ => panic!("expected Random default"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user