mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 13:22:14 +00:00
The cycle/illness/records note DTOs, the biomarker context, and the custom mood/cycle labels each degraded an undecryptable value to a blank/generic fallback with no signal — a key-rotation gap read as 'left blank' and the catch was silent. Keep the fail-soft read posture (one bad row never 500s the list) but emit a wide-event on each swallowed decrypt so a systemic key gap surfaces. The visible per-field 'unreadable' marker in the UI is the deferred remainder.
84 lines
2.5 KiB
TypeScript
84 lines
2.5 KiB
TypeScript
/**
|
|
* Structured-record row → wire-DTO mappers (v1.25, W-RECORDS).
|
|
*
|
|
* The canonical `AllergyDTO` / `FamilyHistoryEntryDTO` shapes the iOS client
|
|
* mirrors (server-authoritative — iOS renders, never recomputes). The
|
|
* sensitive free-text fields live in `*Encrypted` (Bytes) columns and are
|
|
* decrypted fail-soft on read (a key-rotation gap on one row reads `null`,
|
|
* never 500s the whole page) — the IllnessEpisode DTO precedent.
|
|
*/
|
|
import { decryptFromBytes } from "@/lib/ai/coach/bytes-codec";
|
|
import { getEvent } from "@/lib/logging/context";
|
|
import type { Allergy, FamilyHistoryEntry } from "@/generated/prisma/client";
|
|
|
|
export interface AllergyDTO {
|
|
id: string;
|
|
substance: string;
|
|
category: string;
|
|
type: string;
|
|
severity: string | null;
|
|
status: string;
|
|
onsetAt: string | null;
|
|
reaction: string | null;
|
|
note: string | null;
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
}
|
|
|
|
export interface FamilyHistoryEntryDTO {
|
|
id: string;
|
|
relationship: string;
|
|
condition: string;
|
|
ageAtOnset: number | null;
|
|
note: string | null;
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
}
|
|
|
|
/** Decrypt a Bytes payload fail-soft (null on missing / undecryptable). */
|
|
function decryptOptional(buf: Uint8Array | null): string | null {
|
|
if (!buf || buf.byteLength === 0) return null;
|
|
try {
|
|
return decryptFromBytes(buf);
|
|
} catch (err) {
|
|
// Undecryptable payload (key gap / corruption): fail soft to null but log
|
|
// it (F-CRYPTO-2) so a systemic key gap surfaces instead of reading blank.
|
|
getEvent()?.addWarning(
|
|
`records field decrypt failed: ${
|
|
err instanceof Error ? err.message : String(err)
|
|
}`,
|
|
);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export function toAllergyDTO(row: Allergy): AllergyDTO {
|
|
return {
|
|
id: row.id,
|
|
substance: row.substance,
|
|
category: row.category,
|
|
type: row.type,
|
|
severity: row.severity,
|
|
status: row.status,
|
|
onsetAt: row.onsetAt ? row.onsetAt.toISOString() : null,
|
|
reaction: decryptOptional(row.reactionEncrypted),
|
|
note: decryptOptional(row.notesEncrypted),
|
|
createdAt: row.createdAt.toISOString(),
|
|
updatedAt: row.updatedAt.toISOString(),
|
|
};
|
|
}
|
|
|
|
export function toFamilyHistoryEntryDTO(
|
|
row: FamilyHistoryEntry,
|
|
): FamilyHistoryEntryDTO {
|
|
return {
|
|
id: row.id,
|
|
relationship: row.relationship,
|
|
condition: row.condition,
|
|
ageAtOnset: row.ageAtOnset,
|
|
note: decryptOptional(row.notesEncrypted),
|
|
createdAt: row.createdAt.toISOString(),
|
|
updatedAt: row.updatedAt.toISOString(),
|
|
};
|
|
}
|