Skip to main content

qualia_client_core/wellfair/api/
disclosure.rs

1//! Disclosure traceability + encrypted sanctuary vault
2
3use super::super::med_reminders::{
4    compute_due_reminders, load_prefs, save_prefs, DueMedReminder, MedReminderPrefs,
5};
6use super::super::sanctuary::{
7    load_prefs as load_sanctuary_prefs, lock_sanctuary, setup_sanctuary, unlock_sanctuary,
8    SanctuaryPrefs,
9};
10use sha2::{Digest, Sha256};
11use wellfare_core::sleep_analytics::{
12    self, SleepDebtReport, SleepHeatmapReport, SleepNightSample, DEFAULT_TARGET_SLEEP_MIN,
13};
14
15use super::super::personal_profile::{new_contact_id, EmergencyContact, EmergencyContactStore};
16
17use super::*;
18
19impl WebizenHostApi {
20    // --- Disclosure traceability (ADR 0011 D5) + duty of inquiry (D8) ---
21
22    /// Record a **transparency cc** — the protective "I informed authority X for purpose Y" note.
23    pub fn record_transparency_cc(
24        &self,
25        credential_id: &str,
26        informed_authority_did: &str,
27        purpose: &str,
28    ) -> Result<(), String> {
29        let cc = crate::disclosure_trace::TransparencyCc {
30            credential_id: credential_id.to_string(),
31            informed_authority_did: informed_authority_did.to_string(),
32            purpose: purpose.to_string(),
33            informed_unix: Self::now_unix(),
34        };
35        self.accountability_store()?
36            .record_transparency_cc(cc, &self.signing_key, Self::now_unix())
37            .map_err(|e| e.to_string())
38    }
39
40    /// Record a **disclosure event** (an access, or an onward-share if `onward_to` is set). A per-recipient
41    /// fingerprint + id are generated. Returns the recorded event (its `fingerprint` is the tracing anchor).
42    pub fn record_disclosure(
43        &self,
44        commitment_hex: &str,
45        credential_id: &str,
46        recipient_did: &str,
47        acting_delegate_did: Option<String>,
48        onward_to: Option<String>,
49    ) -> Result<crate::disclosure_trace::DisclosureEvent, String> {
50        let commitment = crate::accountability_store::parse_commitment_hex(commitment_hex)?;
51        let now = Self::now_unix();
52        let actor = acting_delegate_did.as_deref().unwrap_or(recipient_did);
53        // Deterministic per-recipient/per-disclosure fingerprint (the traitor-tracing anchor).
54        let digest = Sha256::digest(
55            format!("{}:{recipient_did}:{actor}:{now}", hex::encode(commitment)).as_bytes(),
56        );
57        let mut fingerprint = [0u8; 16];
58        fingerprint.copy_from_slice(&digest[..16]);
59        let id = format!("d-{}", hex::encode(&digest[16..22]));
60        let kind = match onward_to {
61            Some(to_did) => crate::disclosure_trace::DisclosureKind::OnwardShare { to_did },
62            None => crate::disclosure_trace::DisclosureKind::DirectAccess,
63        };
64        let event = crate::disclosure_trace::DisclosureEvent {
65            id,
66            payload_commitment: commitment,
67            credential_id: credential_id.to_string(),
68            recipient_did: recipient_did.to_string(),
69            acting_delegate_did,
70            time_unix: now,
71            fingerprint,
72            kind,
73        };
74        self.accountability_store()?
75            .record_disclosure_event(event.clone(), &self.signing_key, now)
76            .map_err(|e| e.to_string())?;
77        Ok(event)
78    }
79
80    /// The disclosure chain for a payload (who saw it, via which route).
81    pub fn disclosure_chain(
82        &self,
83        commitment_hex: &str,
84    ) -> Result<Vec<crate::disclosure_trace::DisclosureEvent>, String> {
85        let c = crate::accountability_store::parse_commitment_hex(commitment_hex)?;
86        self.accountability_store()?
87            .disclosure_chain(&c)
88            .map_err(|e| e.to_string())
89    }
90
91    /// The distinct actors who had access to a payload — the set a leak must be within.
92    pub fn actors_with_access(&self, commitment_hex: &str) -> Result<Vec<String>, String> {
93        let c = crate::accountability_store::parse_commitment_hex(commitment_hex)?;
94        self.accountability_store()?
95            .actors_with_access(&c)
96            .map_err(|e| e.to_string())
97    }
98
99    /// **Trace a leak** by its fingerprint (hex, 16 bytes) → the disclosure + accountable actor.
100    pub fn trace_leak(
101        &self,
102        fingerprint_hex: &str,
103    ) -> Result<Option<crate::disclosure_trace::DisclosureEvent>, String> {
104        let bytes =
105            hex::decode(fingerprint_hex.trim()).map_err(|e| format!("fingerprint not hex: {e}"))?;
106        let fp: crate::disclosure_trace::DisclosureFingerprint = bytes
107            .as_slice()
108            .try_into()
109            .map_err(|_| "fingerprint must be 16 bytes".to_string())?;
110        self.accountability_store()?
111            .trace_leak(&fp)
112            .map_err(|e| e.to_string())
113    }
114
115    /// List transparency cc records.
116    pub fn list_transparency_ccs(
117        &self,
118    ) -> Result<Vec<crate::disclosure_trace::TransparencyCc>, String> {
119        self.accountability_store()?
120            .list_transparency_ccs()
121            .map_err(|e| e.to_string())
122    }
123
124    /// **Assess a duty of inquiry** — classify conduct against the duty (the fair negligence classifier: was
125    /// an accessible means left unchecked, and did a harmful act follow?). Pure; no persistence.
126    pub fn assess_duty_of_inquiry(
127        &self,
128        duty: crate::duty_of_inquiry::DutyOfInquiry,
129        conduct: crate::duty_of_inquiry::ConductAgainstDuty,
130    ) -> crate::duty_of_inquiry::InquiryVerdict {
131        crate::duty_of_inquiry::assess(&duty, &conduct)
132    }
133
134    pub fn sleep_analytics(
135        &self,
136        target_min: f64,
137    ) -> Result<(SleepDebtReport, SleepHeatmapReport), String> {
138        let sleep_rows = self.list_journal_by_kind("sleep", 128)?;
139        let mut samples = Vec::new();
140        for row in sleep_rows {
141            if let Some(ref summary) = row.summary {
142                if let Some((dur, eff)) = sleep_analytics::parse_sleep_summary_json(summary) {
143                    samples.push(SleepNightSample {
144                        night_unix: row.asserted_time_unix,
145                        duration_min: dur,
146                        efficiency: eff,
147                    });
148                }
149            }
150        }
151        samples.sort_by_key(|s| s.night_unix);
152        let debt = sleep_analytics::compute_sleep_debt(&samples, target_min);
153        let heatmap = sleep_analytics::compute_weekly_heatmap(&samples, target_min);
154        Ok((debt, heatmap))
155    }
156
157    pub fn default_sleep_analytics(&self) -> Result<(SleepDebtReport, SleepHeatmapReport), String> {
158        self.sleep_analytics(DEFAULT_TARGET_SLEEP_MIN)
159    }
160
161    pub fn add_emergency_contact(
162        &self,
163        display_name: &str,
164        relationship: &str,
165        phone: Option<String>,
166        email: Option<String>,
167        notes: Option<String>,
168    ) -> Result<EmergencyContact, String> {
169        let now = Self::now_unix() as u32;
170        let contact = EmergencyContact {
171            id: new_contact_id(display_name, now),
172            display_name: display_name.to_string(),
173            relationship: relationship.to_string(),
174            phone,
175            email,
176            notes,
177            created_at_unix: now,
178        };
179        let store = EmergencyContactStore::open(&self.storage_root).map_err(|e| e.to_string())?;
180        store.append(&contact).map_err(|e| e.to_string())?;
181        Ok(contact)
182    }
183
184    pub fn list_emergency_contacts(&self) -> Result<Vec<EmergencyContact>, String> {
185        let store = EmergencyContactStore::open(&self.storage_root).map_err(|e| e.to_string())?;
186        store.list().map_err(|e| e.to_string())
187    }
188
189    pub fn med_reminder_prefs(&self) -> MedReminderPrefs {
190        load_prefs(&self.storage_root)
191    }
192
193    pub fn set_med_reminders_enabled(&self, enabled: bool) -> Result<MedReminderPrefs, String> {
194        let mut prefs = load_prefs(&self.storage_root);
195        if enabled && !prefs.permission_granted {
196            return Err("Grant reminder permission before enabling notifications".into());
197        }
198        prefs.enabled = enabled;
199        save_prefs(&self.storage_root, &prefs).map_err(|e| e.to_string())?;
200        Ok(prefs)
201    }
202
203    pub fn grant_med_reminder_permission(&self) -> Result<MedReminderPrefs, String> {
204        let mut prefs = load_prefs(&self.storage_root);
205        prefs.permission_granted = true;
206        prefs.permission_granted_at_unix = Some(Self::now_unix() as u32);
207        save_prefs(&self.storage_root, &prefs).map_err(|e| e.to_string())?;
208        Ok(prefs)
209    }
210
211    pub fn list_due_med_reminders(
212        &self,
213        window_minutes: i32,
214    ) -> Result<Vec<DueMedReminder>, String> {
215        let prefs = load_prefs(&self.storage_root);
216        if !prefs.enabled || !prefs.permission_granted {
217            return Ok(Vec::new());
218        }
219        let journal = self
220            .vault
221            .list_health_records(128)
222            .map_err(|e| e.to_string())?;
223        let now = chrono::Local::now().time();
224        Ok(compute_due_reminders(&journal, now, window_minutes))
225    }
226
227    pub fn sanctuary_prefs(&self) -> SanctuaryPrefs {
228        load_sanctuary_prefs(&self.storage_root)
229    }
230
231    pub fn setup_sanctuary(
232        &self,
233        real_pin: &str,
234        decoy_pin: &str,
235    ) -> Result<SanctuaryPrefs, String> {
236        setup_sanctuary(
237            &self.storage_root,
238            real_pin,
239            decoy_pin,
240            Self::now_unix() as u32,
241        )
242    }
243
244    pub fn lock_sanctuary(&self) -> Result<SanctuaryPrefs, String> {
245        lock_sanctuary(&self.storage_root)
246    }
247
248    pub fn unlock_sanctuary(&self, pin: &str) -> Result<SanctuaryPrefs, String> {
249        unlock_sanctuary(&self.storage_root, pin)
250    }
251
252    // --- Encrypted Sanctuary vault (real boundary; native-only, plan §6) ---
253    //
254    // Sensitive free-text notes are stored ONLY inside AEAD-encrypted lane files keyed by a
255    // PBKDF2-derived key — there is no plaintext journal path for them. Nothing is readable
256    // without the PIN, and the decoy PIN opens a separate lane that never aliases real data.
257
258    #[cfg(not(target_arch = "wasm32"))]
259    pub fn sanctuary_vault_configured(&self) -> bool {
260        super::super::sanctuary_vault::is_configured(&self.storage_root)
261    }
262
263    #[cfg(not(target_arch = "wasm32"))]
264    pub fn setup_sanctuary_vault(&self, real_pin: &str, decoy_pin: &str) -> Result<(), String> {
265        super::super::sanctuary_vault::setup(&self.storage_root, real_pin, decoy_pin)
266    }
267
268    /// Verify a PIN and report which lane it opens (real vs duress decoy).
269    #[cfg(not(target_arch = "wasm32"))]
270    pub fn sanctuary_vault_resolve_lane(
271        &self,
272        pin: &str,
273    ) -> Result<super::super::sanctuary_vault::SanctuaryLane, String> {
274        super::super::sanctuary_vault::resolve_lane(&self.storage_root, pin)
275    }
276
277    #[cfg(not(target_arch = "wasm32"))]
278    pub fn add_sanctuary_vault_note(
279        &self,
280        pin: &str,
281        body: &str,
282    ) -> Result<super::super::sanctuary_vault::SanctuaryLane, String> {
283        super::super::sanctuary_vault::add_note(
284            &self.storage_root,
285            pin,
286            body,
287            Self::now_unix() as u32,
288        )
289    }
290
291    #[cfg(not(target_arch = "wasm32"))]
292    pub fn list_sanctuary_vault_notes(
293        &self,
294        pin: &str,
295    ) -> Result<
296        (
297            super::super::sanctuary_vault::SanctuaryLane,
298            Vec<super::super::sanctuary_vault::SanctuaryVaultNote>,
299        ),
300        String,
301    > {
302        super::super::sanctuary_vault::list_notes(&self.storage_root, pin)
303    }
304}