mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 13:01:00 +00:00
The full-backup export (plaintext + passphrase-encrypted, and the weekly auto-backup) only ever covered measurements, medications, mood, and cycle data. Labs, illness episodes, allergies, family history, and workouts were silently missing, and the export UI implied "everything" was included. Adds a shared records-backup section (src/lib/export/records-backup.ts) that both the on-demand export routes and the weekly data-backup worker read from: lab results + the biomarker catalog, illness episodes (including flares via parentConditionId) with their day-logs, allergies, family history, and workout summaries. Every read is owner-scoped and soft-delete-filtered; encrypted free-text columns decrypt fail-soft so one bad row never aborts the whole backup. Document original files and workout GPS/per-sample time series stay deliberately out of the JSON payload (size). The payload now carries an explicit manifest disclosing exactly that, and the export UI states it in plain text instead of implying completeness. These new domains are export-only for now — admin restore still only recreates the original measurements/medications/mood/cycle domains; the payload validates and reports counts for the rest, but restoring a snapshot does not yet recreate them. Also fixes the admin backup-upload total to count every domain, not just the original four.
52 lines
1.9 KiB
TypeScript
52 lines
1.9 KiB
TypeScript
/**
|
|
* v1.17.1 — server-side helpers for the structured lab-result store.
|
|
*
|
|
* The AES-256-GCM ↔ `Bytes` codec for `LabResult.noteEncrypted`. The
|
|
* free-text note is the only sensitive column on the model; it shares the
|
|
* `encrypt()` string format (`"<keyId>.<base64>"`) every other `*Encrypted`
|
|
* column uses, encoded as UTF-8 bytes.
|
|
*/
|
|
import { Buffer } from "node:buffer";
|
|
|
|
import { decrypt, encrypt } from "@/lib/crypto";
|
|
import { getEvent } from "@/lib/logging/context";
|
|
|
|
/** Encrypt a UTF-8 note into the `Bytes` payload the schema stores. */
|
|
export function encryptNoteToBytes(plaintext: string): Uint8Array<ArrayBuffer> {
|
|
const ciphertext = encrypt(plaintext);
|
|
const encoded = Buffer.from(ciphertext, "utf8");
|
|
// Prisma `Bytes` maps to `Uint8Array<ArrayBuffer>`; allocate a fresh
|
|
// ArrayBuffer-backed view so the structural type stays stable.
|
|
const out = new Uint8Array(new ArrayBuffer(encoded.byteLength));
|
|
out.set(encoded);
|
|
return out;
|
|
}
|
|
|
|
/** Decrypt a stored `Bytes` note back to plaintext. Throws on a bad key id. */
|
|
export function decryptNoteFromBytes(buf: Uint8Array): string {
|
|
return decrypt(Buffer.from(buf).toString("utf8"));
|
|
}
|
|
|
|
/**
|
|
* Decrypt a stored `Bytes` note, fail-soft to `null` on any error.
|
|
*
|
|
* Mirrors `decryptContextSoft` in the biomarker store: a single bad-key /
|
|
* malformed row must not fail a bulk read (list view, backup export) that
|
|
* spans many rows. The single-resource GET path uses the throwing
|
|
* `decryptNoteFromBytes` so a genuine decrypt failure there surfaces instead
|
|
* of silently masking a key-rotation gap.
|
|
*/
|
|
export function decryptNoteSoft(buf: Uint8Array | null): string | null {
|
|
if (!buf || buf.byteLength === 0) return null;
|
|
try {
|
|
return decryptNoteFromBytes(buf);
|
|
} catch (err) {
|
|
getEvent()?.addWarning(
|
|
`lab note decrypt failed: ${
|
|
err instanceof Error ? err.message : String(err)
|
|
}`,
|
|
);
|
|
return null;
|
|
}
|
|
}
|