mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 14:16:12 +00:00
fix(labs): fail-soft the biomarker list context decrypt
The catalog GET eagerly threw on the first un-decryptable context row, 500ing the whole list. The list serializer now decrypts fail-soft (null on a bad-key/malformed row) via decryptContextSoft, bounding the fail-closed blast radius to one marker; the single-resource GET stays the authoritative throwing read for the plaintext context.
This commit is contained in:
1 parent
9141b8f74f
commit
6df0c64d85
2 files changed
+31
-5
No files matched your search
@@ -13,7 +13,7 @@ import { auditLog } from "@/lib/auth/audit";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { withIdempotency } from "@/lib/idempotency";
|
||||
import {
|
||||
decryptContextFromBytes,
|
||||
decryptContextSoft,
|
||||
encryptContextToBytes,
|
||||
} from "@/lib/labs/biomarker-store";
|
||||
import { annotate } from "@/lib/logging/context";
|
||||
@@ -22,6 +22,12 @@ import { createBiomarkerSchema } from "@/lib/validations/biomarkers";
|
||||
/**
|
||||
* v1.18.1 — user-scoped Biomarker catalog (`/api/biomarkers`).
|
||||
*
|
||||
* Module gate: like `/api/labs/*`, this is intentionally NOT server-gated.
|
||||
* The Labs module toggle is a UX-only nav preference; the data is owner-scoped
|
||||
* and read by cross-feature surfaces (Vorsorge reminder resolution, the
|
||||
* doctor-report PDF) regardless of the toggle. Deliberate opt-out, in contrast
|
||||
* to the born-gated `/api/illness/*` routes.
|
||||
*
|
||||
* The catalog is the Labs feature's primary object: a marker is defined ONCE
|
||||
* (name, unit, reference bounds, optional context) and every later reading
|
||||
* just picks it. GET lists the caller's markers; POST defines a new one.
|
||||
@@ -33,7 +39,10 @@ import { createBiomarkerSchema } from "@/lib/validations/biomarkers";
|
||||
*/
|
||||
|
||||
// The encrypted context bytes are never echoed back; `hasContext` flags
|
||||
// presence and the single-resource GET returns the decrypted text.
|
||||
// presence. The list path decrypts FAIL-SOFT (`decryptContextSoft` → null on
|
||||
// a bad-key / malformed row) so one un-decryptable row never 500s the whole
|
||||
// catalog — the single-resource GET is the authoritative read for the
|
||||
// plaintext context. This bounds the fail-closed blast radius to one marker.
|
||||
function serialiseBiomarker(row: {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -53,9 +62,7 @@ function serialiseBiomarker(row: {
|
||||
upperBound: row.upperBound,
|
||||
panel: row.panel,
|
||||
hasContext: row.contextEncrypted !== null,
|
||||
context: row.contextEncrypted
|
||||
? decryptContextFromBytes(row.contextEncrypted)
|
||||
: null,
|
||||
context: decryptContextSoft(row.contextEncrypted),
|
||||
createdAt: row.createdAt.toISOString(),
|
||||
updatedAt: row.updatedAt.toISOString(),
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@ import { _resetCryptoCacheForTests } from "@/lib/crypto";
|
||||
|
||||
import {
|
||||
decryptContextFromBytes,
|
||||
decryptContextSoft,
|
||||
encryptContextToBytes,
|
||||
} from "../biomarker-store";
|
||||
|
||||
@@ -32,4 +33,22 @@ describe("biomarker context codec", () => {
|
||||
const asString = Buffer.from(bytes).toString("utf8");
|
||||
expect(asString).not.toContain(plain);
|
||||
});
|
||||
|
||||
describe("decryptContextSoft (fail-soft list decrypt)", () => {
|
||||
it("returns null for a null payload", () => {
|
||||
expect(decryptContextSoft(null)).toBeNull();
|
||||
});
|
||||
|
||||
it("round-trips a valid payload like the throwing variant", () => {
|
||||
const bytes = encryptContextToBytes("morning fasting panel");
|
||||
expect(decryptContextSoft(bytes)).toBe("morning fasting panel");
|
||||
});
|
||||
|
||||
it("returns null instead of throwing on a malformed / bad-key row", () => {
|
||||
// A non-ciphertext payload would throw in `decryptContextFromBytes`.
|
||||
const garbage = new Uint8Array(Buffer.from("not-a-ciphertext"));
|
||||
expect(() => decryptContextFromBytes(garbage)).toThrow();
|
||||
expect(decryptContextSoft(garbage)).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user