mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 13:01:00 +00:00
A new receipt kind, ai_extraction, covers reading documents, lab report scans and typed medication text with an external model. Switching the document auto-read on now mints ai_extraction instead of ai_full, so it no longer grants the Coach or the analysis as a side effect. Existing ai_full receipts keep covering everything. DELETE /api/consent/ai/latest now runs through withdrawConsent: the revoke and, when it leaves no receipt covering the analysis, the deletion of the text a model wrote under it commit in one transaction. That is the status notes, the cached briefing, model-written period narratives, reaction lines and workout paragraphs. Coach conversations, facts and plans and document summaries stay. The purge is audit-logged as consent.ai.purge with counts only. Refs #975
88 lines
3.5 KiB
TypeScript
88 lines
3.5 KiB
TypeScript
import { z } from "zod/v4";
|
|
|
|
/**
|
|
* v1.4.40 SB-10 — AI consent receipts (App-Store Guideline 5.1.2(i) +
|
|
* GDPR Art. 7 audit trail).
|
|
*
|
|
* The discriminator covers the consent surfaces collected independently:
|
|
* - `ai_full` full assistant + on-device personalisation
|
|
* - `ai_insights_only` read-only Insights generation (no Coach)
|
|
* - `ai_coach` chat-style Coach access
|
|
* - `ai_extraction` reading documents, lab report scans and typed
|
|
* medication text with an external model (minted by
|
|
* the document auto-read toggle; it satisfies neither
|
|
* the Coach nor the analysis)
|
|
*
|
|
* Keeping each surface as its own row means the legal team can prove
|
|
* which exact scope the user agreed to without parsing the signed
|
|
* artefact server-side. The server stays opaque to the artefact's
|
|
* format — we accept either a base64-encoded PDF or a signed JWT, the
|
|
* audit trail *is* the storage plus the `signedAt` timestamp.
|
|
*/
|
|
export const consentKindEnum = z.enum([
|
|
"ai_full",
|
|
"ai_insights_only",
|
|
"ai_coach",
|
|
"ai_extraction",
|
|
]);
|
|
|
|
export type ConsentKind = z.infer<typeof consentKindEnum>;
|
|
|
|
/**
|
|
* 64 KB upper bound on the artefact. A signed PDF receipt sits well
|
|
* under 32 KB; a signed JWT is a few hundred bytes. The cap exists so
|
|
* a misbehaving client (or a malicious one) can't fill the audit table
|
|
* with multi-megabyte rows.
|
|
*
|
|
* v1.4.40 security M1 — the cap is enforced via `Buffer.byteLength(...,
|
|
* "utf8")` because `z.string().max()` counts JavaScript string units
|
|
* (UTF-16 code units), not bytes. A UTF-8 artefact full of multi-byte
|
|
* code points (CJK, emoji) would otherwise be allowed past the 64 KB
|
|
* row budget — the database stores bytes, not code units, and the
|
|
* audit-table guarantee is byte-bounded.
|
|
*/
|
|
const ARTEFACT_MAX_BYTES = 64 * 1024;
|
|
|
|
export const consentPostBody = z.object({
|
|
kind: consentKindEnum,
|
|
artefact: z
|
|
.string()
|
|
.min(1, "artefact must not be empty")
|
|
.refine((value) => Buffer.byteLength(value, "utf8") <= ARTEFACT_MAX_BYTES, {
|
|
message: "artefact exceeds 64 KB cap (UTF-8 byte length)",
|
|
}),
|
|
signedAt: z.iso.datetime({ offset: true }).transform((s) => new Date(s)),
|
|
});
|
|
|
|
export type ConsentPostBody = z.infer<typeof consentPostBody>;
|
|
|
|
/**
|
|
* Optional body for `POST /api/consent/ai/web`. The AI-settings mount posts
|
|
* no body at all (a heal: mints only for an account with no consent history);
|
|
* the explicit re-grant control on the same surface posts
|
|
* `{ intent: "affirmative" }`, the user's own consent act, which may
|
|
* supersede an earlier revocation the way any first grant would. The enum
|
|
* must stay in lockstep with `WebConsentGrantIntent` in
|
|
* `src/lib/consent/web-grant.ts`; the route narrows the parsed value to that
|
|
* type, so a drift fails the typecheck.
|
|
*/
|
|
export const webConsentGrantBody = z.object({
|
|
intent: z.enum(["heal", "affirmative"]).default("heal"),
|
|
});
|
|
|
|
export type WebConsentGrantBodyInput = z.input<typeof webConsentGrantBody>;
|
|
|
|
/**
|
|
* Query schema for both `/api/consent/ai/latest` GET and DELETE.
|
|
*
|
|
* Omitting `kind` on GET returns the latest active receipt per kind
|
|
* (a `Record<ConsentKind, …>` shape). Omitting it on DELETE revokes
|
|
* the latest receipt across every kind — handy for the iOS
|
|
* "deaktivieren alle KI-Funktionen" master toggle.
|
|
*/
|
|
export const consentLatestQuery = z.object({
|
|
kind: consentKindEnum.optional(),
|
|
});
|
|
|
|
export type ConsentLatestQuery = z.infer<typeof consentLatestQuery>;
|