feat(documents): gate document, lab and medication reading on their capabilities

Reading a document, scanning a lab report and turning a medication
description into a schedule follow documentAi, labsOcr and
medicationExtract. Unavailable, the action is not offered. With only the
document-reading consent missing, the place that reads asks for it and
grants exactly that (ai_extraction), nothing wider.

Refusals are read by meta.errorCode: a 403 is no longer always called a
missing consent, and a spent daily budget no longer shows the rate-limit
sentence.
This commit is contained in:
Marc-André Bombeck committed 2026-09-24 11:05:49 +02:00
1 parent 48d97ef70d
commit 4449b34ac7
15 files changed
+466 -40

No files matched your search

@@ -0,0 +1,47 @@
"use client";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { apiPost } from "@/lib/api/api-fetch";
import { queryKeys } from "@/lib/query-keys";
/**
* Grant the consent for reading documents (`ai_extraction`) from the place
* that needs it.
*
* Sending a document, a lab report scan or a typed medication description to
* an AI service outside this server needs its own receipt. The narrow kind
* covers exactly that and never the Coach or AI analysis, so asking for it
* where a document is being read asks for no more than the reader is about to
* do. The tap on the prompt IS the consent act; the receipt records it with
* its source, the same way the web settings record a full grant.
*
* On success `/api/auth/me` is refetched, because the capabilities it
* publishes (`documentAi`, `labsOcr`, `medicationExtract`) are what every
* reading surface renders from.
*/
export function useGrantDocumentReadingConsent() {
const queryClient = useQueryClient();
return useMutation({
mutationKey: queryKeys.aiExtractionConsentGrant(),
mutationFn: async () => {
const signedAt = new Date().toISOString();
return apiPost("/api/consent/ai", {
kind: "ai_extraction",
signedAt,
artefact: JSON.stringify({
source: "web",
kind: "ai_extraction",
grantedAt: signedAt,
note: "In-app affirmative consent to reading documents, granted via the web client.",
}),
});
},
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: queryKeys.authMe() });
void queryClient.invalidateQueries({
queryKey: queryKeys.aiConsentReceipt("ai_extraction"),
});
},
});
}