mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 20:42:43 +00:00
refactor(labs): drop the fail-soft lab-note decrypt, which has no caller left
The export was its only user, and the export now reads through a helper that marks an undecryptable note instead of blanking it. Nothing else wanted the silent null: the list DTO never carries the note at all, only `hasNote`, and the single-resource GET has always used the throwing decrypt so a key gap surfaces there. Deleted rather than kept behind a dead-code exemption. A fail-soft twin sitting unused next to a strict one is the exact shape this branch has been clearing out — the next person writing an export reaches for whatever is nearest, and the whole point was that the nearest thing should not silently swallow a row. It had no test of its own, so none was removed; the three comments that pointed at it are rewritten to say what is true now.
This commit is contained in:
1 parent
ae731b92d9
commit
83b1400e5b
4 files changed
+19
-36
No files matched your search
@@ -50,13 +50,13 @@ export const WORKOUTS_MANIFEST_NOTE =
|
||||
* Decrypt an encrypted free-text column for a portable export, or say plainly
|
||||
* that this one could not be read.
|
||||
*
|
||||
* `decryptNoteSoft` / `decryptContextSoft` answer `null` on a wrong or dropped
|
||||
* key. In a list view that is right — one bad row must not take the page down.
|
||||
* In an export it is not: `null` is exactly what a column that was never
|
||||
* written looks like, so the restore importer writes nothing and a note the
|
||||
* person did write disappears without anyone being told. The Coach transcript
|
||||
* and the doctor report both refuse that trade, one with a placeholder
|
||||
* sentence and one with a rendered flag; this follows the same rule.
|
||||
* A fail-soft decrypt answers `null` on a wrong or dropped key. In a list view
|
||||
* that is right — one bad row must not take the page down. In an export it is
|
||||
* not: `null` is exactly what a column that was never written looks like, so
|
||||
* the restore importer writes nothing and a note the person did write
|
||||
* disappears without anyone being told. The Coach transcript and the doctor
|
||||
* report both refuse that trade, one with a placeholder sentence and one with
|
||||
* a rendered flag; this follows the same rule.
|
||||
*
|
||||
* The warning the soft helpers emitted is kept, so a systemic key gap still
|
||||
* shows up in the wide event rather than only in the file.
|
||||
|
||||
@@ -33,10 +33,12 @@ export function decryptContextFromBytes(buf: Uint8Array): string {
|
||||
|
||||
/** Decrypt a stored `Bytes` context note, fail-soft to `null` on any error.
|
||||
*
|
||||
* Mirrors `decryptNoteSoft` in the illness DTO layer: a single bad-key /
|
||||
* malformed row must not 500 the whole catalog list. The single-resource GET
|
||||
* path uses the throwing `decryptContextFromBytes` so a genuine decrypt
|
||||
* failure there surfaces instead of silently masking a key-rotation gap. */
|
||||
* A single bad-key / malformed row must not 500 the whole catalog list, and
|
||||
* the list surface renders a missing context as nothing either way. The
|
||||
* single-resource GET path uses the throwing `decryptContextFromBytes` so a
|
||||
* genuine decrypt failure there surfaces instead of silently masking a
|
||||
* key-rotation gap, and the backup export reads through its own helper that
|
||||
* marks the row rather than blanking it. */
|
||||
export function decryptContextSoft(buf: Uint8Array | null): string | null {
|
||||
if (!buf) return null;
|
||||
try {
|
||||
|
||||
+5
-24
@@ -5,11 +5,15 @@
|
||||
* 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.
|
||||
*
|
||||
* There is one decrypt here and it throws. A fail-soft twin used to sit
|
||||
* beside it for bulk reads; the list DTO never carries the note (it reports
|
||||
* `hasNote` only) and the export now writes a visible marker instead of a
|
||||
* null, so nothing was left that wanted a silent one.
|
||||
*/
|
||||
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> {
|
||||
@@ -26,26 +30,3 @@ export function encryptNoteToBytes(plaintext: string): Uint8Array<ArrayBuffer> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -2627,7 +2627,7 @@ describe("every model the plan claims two-ended survives a real restore", () =>
|
||||
* A note this instance can no longer decrypt leaves the export as a marker,
|
||||
* not as nothing.
|
||||
*
|
||||
* `decryptNoteSoft` answers `null` on a wrong or dropped key, and in a
|
||||
* A fail-soft decrypt answers `null` on a wrong or dropped key, and in a
|
||||
* portable file `null` is byte-for-byte what a note that was never written
|
||||
* looks like. The restore importer then writes no note, so a note the person
|
||||
* did write is gone and no one is told — the whole loss fits inside a
|
||||
|
||||
Reference in new issue
Block a user