fix(ai): gate medication free-text extraction on active consent

The medication NL-extraction route egresses user-typed free text (PHI) to the
operator's server-managed provider key; require an active consent receipt for
the coach surface before the chain runs, matching the other server-managed
egress sites.
This commit is contained in:
Marc-André Bombeck committed 2026-06-05 03:57:39 +02:00
1 parent 3ce0b69802
commit 200cb5b996
2 files changed
+20

No files matched your search

@@ -43,6 +43,14 @@ vi.mock("@/lib/ai/provider", () => ({
resolveProviderChain: vi.fn(async () => []),
}));
// v1.12.1 — free-text medication extraction egresses PHI to the operator's
// server-managed key, so it now passes through the consent gate. Mock it to a
// no-op here; the gate's own fail-closed behaviour is covered by
// `consent-guard.test.ts`.
vi.mock("@/lib/ai/consent-guard", () => ({
assertConsentForChain: vi.fn(async () => undefined),
}));
vi.mock("@/lib/ai/provider-runner", async () => {
const actual =
await vi.importActual<typeof import("@/lib/ai/provider-runner")>(
@@ -69,6 +77,7 @@ import {
resolveProviderChain,
} from "@/lib/ai/provider";
import { runRawCompletionWithFallback } from "@/lib/ai/provider-runner";
import { assertConsentForChain } from "@/lib/ai/consent-guard";
function postReq(body: unknown): NextRequest {
return new NextRequest("http://localhost/api/medications/extract", {
@@ -175,6 +184,11 @@ describe("POST /api/medications/extract — happy path", () => {
doseUnit: "mg",
cadenceKind: "everyNWeeks",
});
// The PHI free-text egress is gated on an active consent receipt for the
// coach surface before the provider chain runs.
expect(assertConsentForChain).toHaveBeenCalledWith(
expect.objectContaining({ surface: "coach" }),
);
});
it("strips a hallucinated name that is absent from the user's text", async () => {
+6
View File
@@ -57,6 +57,7 @@ import {
runRawCompletionWithFallback,
} from "@/lib/ai/provider-runner";
import { resolveProvider, resolveProviderChain } from "@/lib/ai/provider";
import { assertConsentForChain } from "@/lib/ai/consent-guard";
import {
buildDateKey,
@@ -167,6 +168,11 @@ async function handleExtract(request: NextRequest): Promise<Response> {
chain.push({ providerType: "admin-openai", instance: legacy });
}
// Free-text medication input is PHI. If the resolved chain could egress
// via the operator's server-managed key, an active consent receipt is
// required first — same gate as the Coach (which shares this chain).
await assertConsentForChain({ userId, chain, surface: "coach" });
const today = todayOverride ?? buildDateKey();
const { systemPrompt, userPrompt } = buildMedicationExtractionPrompt({
text,