diff --git a/src/app/api/medications/extract/__tests__/route.test.ts b/src/app/api/medications/extract/__tests__/route.test.ts index a570df213..34006be4a 100644 --- a/src/app/api/medications/extract/__tests__/route.test.ts +++ b/src/app/api/medications/extract/__tests__/route.test.ts @@ -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( @@ -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 () => { diff --git a/src/app/api/medications/extract/route.ts b/src/app/api/medications/extract/route.ts index 34994079d..901456274 100644 --- a/src/app/api/medications/extract/route.ts +++ b/src/app/api/medications/extract/route.ts @@ -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 { 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,