feat(vault): seal transcript/notes/summary at rest via storage read/write helpers (T8.8)

This commit is contained in:
iamdoubz committed 2026-07-03 08:06:05 -05:00
1 parent f12e86513a
commit 0f7af4576e
1 file changed
+40 -20
+40 -20
View File
@@ -477,21 +477,19 @@ impl SqliteStore {
.fetch_one(&self.pool)
.await?;
let transcript_text =
std::fs::read_to_string(paths::meeting_dir(id).join("transcript.json"))
.ok()
.and_then(|s| serde_json::from_str::<TranscriptFile>(&s).ok())
.map(|t| {
t.segments
.iter()
.map(|s| s.text.as_str())
.collect::<Vec<_>>()
.join(" ")
})
.unwrap_or_default();
let transcript_text = read_artifact(&paths::meeting_dir(id).join("transcript.json"))
.and_then(|s| serde_json::from_str::<TranscriptFile>(&s).ok())
.map(|t| {
t.segments
.iter()
.map(|s| s.text.as_str())
.collect::<Vec<_>>()
.join(" ")
})
.unwrap_or_default();
let notes_text =
std::fs::read_to_string(paths::meeting_dir(id).join("notes.md")).unwrap_or_default();
read_artifact(&paths::meeting_dir(id).join("notes.md")).unwrap_or_default();
sqlx::query("DELETE FROM meeting_fts WHERE meeting_id = ?")
.bind(id)
@@ -664,7 +662,10 @@ impl Store for SqliteStore {
};
let json =
serde_json::to_string_pretty(&transcript).map_err(|e| StoreError::Db(e.to_string()))?;
std::fs::write(paths::meeting_dir(id).join("transcript.json"), json)?;
write_artifact(
&paths::meeting_dir(id).join("transcript.json"),
json.as_bytes(),
)?;
self.reindex_fts(id).await?;
let template_id: Option<String> =
sqlx::query_scalar("SELECT template_id FROM meetings WHERE id = ?")
@@ -778,8 +779,7 @@ impl Store for SqliteStore {
.collect();
let folder = paths::meeting_dir(id);
let mut segments = std::fs::read_to_string(folder.join("transcript.json"))
.ok()
let mut segments = read_artifact(&folder.join("transcript.json"))
.and_then(|s| serde_json::from_str::<TranscriptFile>(&s).ok())
.map(|t| t.segments)
.unwrap_or_default();
@@ -791,9 +791,8 @@ impl Store for SqliteStore {
seg.speaker = resolve_canonical(&seg.speaker, &merge_map).to_string();
}
}
let notes_markdown = std::fs::read_to_string(folder.join("notes.md")).unwrap_or_default();
let summary = std::fs::read_to_string(folder.join("summary.json"))
.ok()
let notes_markdown = read_artifact(&folder.join("notes.md")).unwrap_or_default();
let summary = read_artifact(&folder.join("summary.json"))
.and_then(|s| serde_json::from_str::<SummaryFile>(&s).ok());
let tags = self.tags_for_meeting(id).await?;
@@ -837,7 +836,10 @@ impl Store for SqliteStore {
}
async fn update_notes(&self, id: &MeetingId, markdown: &str) -> Result<(), StoreError> {
std::fs::write(paths::meeting_dir(id).join("notes.md"), markdown)?;
write_artifact(
&paths::meeting_dir(id).join("notes.md"),
markdown.as_bytes(),
)?;
sqlx::query("UPDATE meetings SET updated_at = ? WHERE id = ?")
.bind(now_unix())
.bind(id)
@@ -1450,6 +1452,24 @@ impl Store for SqliteStore {
}
}
/// Read a derived artifact, transparently decrypting it if the vault sealed it
/// (T8.8, FR-SEC-3). Returns `None` if the file is missing or can't be
/// decrypted (vault locked / wrong key) — callers already treat a missing
/// artifact as empty, so a locked vault degrades to "no content" rather than a
/// crash.
fn read_artifact(path: &std::path::Path) -> Option<String> {
let bytes = std::fs::read(path).ok()?;
let plain = crate::vault::open(&bytes).ok()?;
Some(String::from_utf8_lossy(&plain).into_owned())
}
/// Write a derived artifact, sealing it when the vault is unlocked (a no-op
/// passthrough otherwise). Audio is not sealed here — it's handled separately.
fn write_artifact(path: &std::path::Path, contents: &[u8]) -> std::io::Result<()> {
let sealed = crate::vault::seal(contents).map_err(|e| std::io::Error::other(e.to_string()))?;
std::fs::write(path, sealed)
}
fn dir_size(path: &str) -> u64 {
std::fs::read_dir(path)
.into_iter()